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<T = unknown> {
  /**
   * The action name the activity is registered for.
   */
  action: string
  /**
   * The data passed to the activity.
   */
  data?: T
  /**
   * The category of the activity.
   * Optional and currently not used.
   */
  category?: string[]
  /**
   * The flags that control the behavior of the activity.
   */
  flags?: number
  /**
   * The request code used to distinguish between different activities.
   */
  requestCode?: number
}

/**
 * An intent returned by an activity after the activity result is ready.
 * This object is only created when the activity is started with the `ForResult` flag.
 */
export interface ResolvedIntent<T = unknown> extends Intent<T> {
  /**
   * The result code returned by the activity.
   */
  result: IntentResult
}

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
}

/**
 * Type for activity constructor with static action property.
 */
export interface ActivityConstructor {
  new (parent: Application): Activity
  action?: string
}

/**
 * Enhanced activity registration options.
 */
export interface ActivityRegistrationOptions {
  /** Whether to override existing registrations */
  override?: boolean
  /** Optional validation function for intents */
  validateIntent?: (intent: Intent) => boolean
}

/**
 * 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>()

  /**
   * Stores the resolvers for activities started for result.
   */
  private resultResolvers = new Map<Intent, (intent: ResolvedIntent) => void>()

  /**
   * 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)
  }

  /**
   * Registers an activity class with enhanced options.
   * @param action The action name to register the activity for.
   * @param activityClass The activity class constructor.
   * @param options Optional registration configuration.
   */
  registerActivity(
    action: string,
    activityClass: ActivityConstructor,
    options: ActivityRegistrationOptions = {}
  ): void {
    const { override = false } = options

    if (this.activityClasses.has(action) && !override) {
      // eslint-disable-next-line no-console
      console.warn(`Activity with action "${action}" already registered.`, new Error().stack)
      return
    }

    // Validate that the class extends Activity
    if (typeof activityClass !== 'function') {
      throw new Error(`Invalid activity class for action "${action}": must be a constructor function`)
    }

    this.activityClasses.set(action, activityClass as typeof Activity)
  }

  /**
   * 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<Activity> {
    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 info.activity
    }
    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
    }
    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 activity
    }
    // this.setupRoute(intent)
    return activity
  }

  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.
   *
   * @template T The type of the result data expected.
   * @param intent The intent to start the activity.
   * @returns A promise that resolves when the started activity finishes, returning the result Intent with
   * data of type T.
   * @example
   * ```typescript
   * const resultIntent = await mgr.startActivityForResult<{ userId: string }>({ action: 'pickUser' });
   * if (resultIntent.resultCode === IntentResult.RESULT_OK) {
   *   console.log(resultIntent.data?.userId);
   * }
   * ```
   */
  async startActivityForResult<T>(intent: Intent): Promise<ResolvedIntent<T | undefined>> {
    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
    }

    return new Promise<ResolvedIntent<T>>((resolve, reject) => {
      this.resultResolvers.set(deepCopy, resolve as (intent: ResolvedIntent<unknown>) => void)
      this.startActivity(deepCopy).catch((e) => {
        this.resultResolvers.delete(deepCopy)
        reject(e)
      })
    })
  }

  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

      // Clean up resources to prevent memory leaks
      this.cleanupActivity(stackEntry.activity)

      this.resolveActivityResult(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.resolveActivityResult(activity, { action: '' })

      // 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 resolveActivityResult(activity: Activity, originalIntent: Intent): void {
    const resolver = this.resultResolvers.get(originalIntent)
    // The activity might have not been started with startActivityForResult.
    // A thing to consider for the future is whether we should throw an error in a case where
    // the activity was started for result but the resolver was not found.
    if (resolver) {
      this.resultResolvers.delete(originalIntent)
      // We construct the result intent.
      // Ideally we would like to preserve the original intent structure but with new data.
      const resultIntent: ResolvedIntent = {
        ...originalIntent,
        data: activity.getResult(),
        result: activity.resultCode,
      }
      resolver(resultIntent)
    }
  }

  private async bringLastActivityToFront(): Promise<void> {
    if (this.modalActivityStack.length > 0) {
      const previousEntry = this.modalActivityStack[this.activityStack.length - 1]
      await this.updateCurrentActivity(previousEntry.activity, false, previousEntry.intent)
    } else if (this.activityStack.length > 0) {
      const previousEntry = this.activityStack[this.activityStack.length - 1]
      await this.updateCurrentActivity(previousEntry.activity, false, previousEntry.intent)
    } 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, intent?: Intent): 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
    }
    switch (activity.lifecycle) {
      case ActivityLifecycle.Paused:
        if (!(await this.transitionStartedToResumed(activity))) {
          return
        }
        break

      case ActivityLifecycle.Stopped:
        await this.safeExecuteLifecycleMethod(() => activity.onRestart(), activity, 'onRestart')
        activity.lifecycle = ActivityLifecycle.Resumed
        break

      case ActivityLifecycle.Destroyed:
        throw new Error(`Invalid state. The activity is already destroyed.`)

      case ActivityLifecycle.Created:
        if (!(await this.transitionCreatedToResumed(activity))) {
          return
        }
        break

      case ActivityLifecycle.Started:
        if (!(await this.transitionStartedToResumed(activity))) {
          return
        }
        break

      case ActivityLifecycle.Initialized:
        if (!(await this.transitionInitializedToResumed(activity, intent))) {
          return
        }
        break
    }
    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
    }
  }

  /**
   * Safely executes an async lifecycle method with error handling.
   * @param lifecycleMethod The lifecycle method to execute.
   * @param activity The activity instance.
   * @param methodName The name of the method for error reporting.
   * @returns Promise that resolves when the method completes or rejects with wrapped error.
   */
  private async safeExecuteLifecycleMethod(
    lifecycleMethod: () => void | Promise<void>,
    activity: Activity,
    methodName: string
  ): Promise<void> {
    try {
      await lifecycleMethod()
    } catch (error) {
      const activityName = activity.constructor.name
      const wrappedError = new Error(
        `Error in ${activityName}.${methodName}(): ${error instanceof Error ? error.message : String(error)}`
      )
      wrappedError.cause = error
      // Allow the activity to handle its own errors, but still propagate
      this.#parent.dispatchEvent(
        new CustomEvent('activity-lifecycle-error', {
          detail: { activity, methodName, error: wrappedError },
        })
      )
      throw wrappedError
    }
  }

  /**
   * Allows navigating back through the activity stack.
   * Call `ActivityManager.navigateBack()` when the user performs a back action
   * (e.g., clicks a back button).
   * @returns Promise that resolves to true if navigation was successful, false if at root.
   */
  async navigateBack(): Promise<boolean> {
    // Handle modal activities first
    if (this.modalActivityStack.length > 0) {
      const topModal = this.modalActivityStack[this.modalActivityStack.length - 1]
      await this.finishActivity(topModal.activity)
      return true
    }

    // Handle regular activities
    if (this.activityStack.length > 1 && this.currentActivity) {
      await this.finishActivity(this.currentActivity)
      return true
    }

    // At root, cannot navigate back
    return false
  }

  /**
   * Navigates to a specific activity in the stack, clearing activities above it.
   * @param action The action of the activity to navigate to.
   * @returns Promise that resolves to true if navigation was successful.
   */
  async navigateToActivity(action: string): Promise<boolean> {
    const index = this.activityStack.findIndex((entry) => entry.action === action)
    if (index === -1) {
      return false
    }

    // Finish all activities above the target
    const activitiesToFinish = this.activityStack.slice(index + 1)
    for (const entry of activitiesToFinish) {
      await this.finishActivity(entry.activity)
    }

    // Bring the target activity to the front
    const targetEntry = this.activityStack[index]
    await this.updateCurrentActivity(targetEntry.activity, false, targetEntry.intent)
    return true
  }

  /**
   * Clears the entire activity stack and starts a new root activity.
   * @param intent The intent for the new root activity.
   */
  async clearStackAndStart(intent: Intent): Promise<void> {
    // Finish all activities
    const allActivities = [...this.activityStack, ...this.modalActivityStack]
    for (const entry of allActivities) {
      if (entry.activity.lifecycle !== ActivityLifecycle.Destroyed) {
        await this.finishActivity(entry.activity)
      }
    }

    // Clear stacks
    this.activityStack.length = 0
    this.modalActivityStack.length = 0
    this.currentActivity = undefined

    // Start new root activity
    await this.startActivity(intent)
  }

  /**
   * Creates an intent with better type safety and validation.
   * @param action The action name.
   * @param data Optional intent data.
   * @param options Optional intent configuration.
   * @returns A properly formed intent.
   */
  createIntent<T = unknown>(
    action: string,
    data?: T,
    options: {
      category?: string[]
      flags?: number
      requestCode?: number
    } = {}
  ): Intent<T> {
    if (!this.isActionRegistered(action)) {
      throw new Error(`Cannot create intent for unregistered action: ${action}`)
    }

    return {
      action,
      data,
      ...options,
    }
  }

  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
  }

  /**
   * Transitions an activity from Created to Resumed state.
   * @param activity The activity to transition.
   * @returns true if successful, false if activity was destroyed during transition.
   */
  private async transitionCreatedToResumed(activity: Activity): Promise<boolean> {
    await this.safeExecuteLifecycleMethod(() => activity.onStart(), activity, 'onStart')
    if (this.isDestroyed(activity)) {
      return false
    }
    activity.lifecycle = ActivityLifecycle.Started

    await this.safeExecuteLifecycleMethod(() => activity.onResume(), activity, 'onResume')
    if (this.isDestroyed(activity)) {
      return false
    }
    activity.lifecycle = ActivityLifecycle.Resumed
    activity.requestUpdate()
    return true
  }

  /**
   * Transitions an activity from Started to Resumed state.
   * @param activity The activity to transition.
   * @returns true if successful, false if activity was destroyed during transition.
   */
  private async transitionStartedToResumed(activity: Activity): Promise<boolean> {
    await this.safeExecuteLifecycleMethod(() => activity.onResume(), activity, 'onResume')
    if (this.isDestroyed(activity)) {
      return false
    }
    activity.lifecycle = ActivityLifecycle.Resumed
    activity.requestUpdate()
    return true
  }

  /**
   * Transitions an activity from Initialized to Resumed state.
   * @param activity The activity to transition.
   * @param intent The intent used to create the activity.
   * @returns true if successful, false if activity was destroyed during transition.
   */
  private async transitionInitializedToResumed(activity: Activity, intent?: Intent): Promise<boolean> {
    await this.safeExecuteLifecycleMethod(() => activity.onCreate(intent), activity, 'onCreate')
    activity.lifecycle = ActivityLifecycle.Created
    if (this.isDestroyed(activity)) {
      return false
    }

    return await this.transitionCreatedToResumed(activity)
  }

  /**
   * Cleans up resources for destroyed activities to prevent memory leaks.
   * @param activity The activity to clean up.
   */
  private cleanupActivity(activity: Activity): void {
    // Remove any event listeners that might create memory leaks
    // This changes the entire prototype chain of the activity,
    // effectively making it a new object. We can't do that as the activity will loose some
    // of its properties, so we don't do it for now.
    // if (activity instanceof EventTarget) {
    //   // Clear all event listeners by replacing the activity with a new EventTarget
    //   // This is a defensive approach since we can't enumerate listeners
    //   Object.setPrototypeOf(activity, EventTarget.prototype)
    // }

    // Clear any references that might prevent garbage collection
    activity.renderRoot = undefined
  }

  /**
   * Validates that an activity stack is in a consistent state.
   * @param stack The activity stack to validate.
   * @returns Array of validation errors, empty if valid.
   */
  private validateStackConsistency(stack: ActivityStackEntry[]): string[] {
    const errors: string[] = []
    const seenIds = new Set<string>()

    for (const entry of stack) {
      if (seenIds.has(entry.id)) {
        errors.push(`Duplicate activity ID found: ${entry.id}`)
      }
      seenIds.add(entry.id)

      if (!entry.activity || !entry.id || !entry.action) {
        errors.push(`Invalid stack entry: missing required properties`)
      }

      if (entry.activity.lifecycle === ActivityLifecycle.Destroyed) {
        errors.push(`Destroyed activity found in stack: ${entry.id}`)
      }
    }

    return errors
  }

  /**
   * Gets diagnostic information about the current state of the activity manager.
   * Useful for debugging and monitoring.
   * @returns Object containing current state information.
   */
  getDiagnostics(): {
    totalActivities: number
    modalActivities: number
    currentActivity?: string
    topActivity?: string
    stackValidation: string[]
    modalStackValidation: string[]
  } {
    return {
      totalActivities: this.activityStack.length,
      modalActivities: this.modalActivityStack.length,
      currentActivity: this.currentActivity?.constructor.name,
      topActivity: this.getTopActivity()?.constructor.name,
      stackValidation: this.validateStackConsistency(this.activityStack),
      modalStackValidation: this.validateStackConsistency(this.modalActivityStack),
    }
  }

  /**
   * Gets all registered activity actions.
   * @returns Array of registered action names.
   */
  getRegisteredActions(): string[] {
    return Array.from(this.activityClasses.keys())
  }

  /**
   * Checks if an action is registered.
   * @param action The action to check.
   * @returns true if the action is registered, false otherwise.
   */
  isActionRegistered(action: string): boolean {
    return this.activityClasses.has(action)
  }
}
