import { Device } from './Device'
import { BleError } from './BleError'
import type { BleManager } from './BleManager'
import type { DeviceId, Subscription, ReconnectionOptions } from './TypeDefinition'

/**
 * Reconnection state for a device
 */
interface ReconnectionState {
  deviceId: DeviceId
  options: ReconnectionOptions
  isReconnecting: boolean
  retryCount: number
  timeoutId?: ReturnType<typeof setTimeout>
  disconnectSubscription?: Subscription
  onReconnect?: (device: Device) => void
  onReconnectFailed?: (deviceId: DeviceId, error: BleError) => void
  cancelled: boolean
}

/**
 * Event callbacks for reconnection events
 */
export interface ReconnectionCallbacks {
  /**
   * Called when a device successfully reconnects
   */
  onReconnect?: (device: Device) => void

  /**
   * Called when reconnection fails after all retries
   */
  onReconnectFailed?: (deviceId: DeviceId, error: BleError) => void

  /**
   * Called when a reconnection attempt starts
   */
  onReconnecting?: (deviceId: DeviceId, attempt: number, maxAttempts: number) => void
}

/**
 * ReconnectionManager automatically handles reconnection when devices disconnect unexpectedly.
 *
 * Features:
 * - Automatic reconnection with exponential backoff
 * - Configurable retry limits and delays
 * - Event callbacks for monitoring reconnection status
 * - Per-device configuration
 *
 * @example
 * const reconnectionManager = new ReconnectionManager(bleManager);
 *
 * // Enable auto-reconnect for a device
 * reconnectionManager.enableAutoReconnect('AA:BB:CC:DD:EE:FF', {
 *   maxRetries: 10,
 *   initialDelayMs: 1000,
 *   maxDelayMs: 60000,
 *   backoffMultiplier: 1.5
 * }, {
 *   onReconnect: (device) => console.log('Reconnected!', device.id),
 *   onReconnectFailed: (deviceId, error) => console.log('Failed to reconnect', deviceId)
 * });
 *
 * // Disable auto-reconnect
 * reconnectionManager.disableAutoReconnect('AA:BB:CC:DD:EE:FF');
 */
export class ReconnectionManager {
  private _manager: BleManager
  private _devices: Map<DeviceId, ReconnectionState> = new Map()
  private _globalCallbacks: ReconnectionCallbacks = {}

  constructor(manager: BleManager) {
    this._manager = manager
  }

  /**
   * Set global callbacks for all reconnection events.
   *
   * @param callbacks Callback functions for reconnection events
   */
  setGlobalCallbacks(callbacks: ReconnectionCallbacks): void {
    this._globalCallbacks = callbacks
  }

  /**
   * Enable automatic reconnection for a device.
   *
   * @param deviceId Device identifier
   * @param options Reconnection options (retries, delays, etc.)
   * @param callbacks Optional callbacks for this specific device
   */
  enableAutoReconnect(deviceId: DeviceId, options?: ReconnectionOptions, callbacks?: ReconnectionCallbacks): void {
    // If already enabled, disable first
    if (this._devices.has(deviceId)) {
      this.disableAutoReconnect(deviceId)
    }

    const reconnectionOptions: ReconnectionOptions = {
      maxRetries: options?.maxRetries ?? 5,
      initialDelayMs: options?.initialDelayMs ?? 1000,
      maxDelayMs: options?.maxDelayMs ?? 30000,
      backoffMultiplier: options?.backoffMultiplier ?? 2,
      connectionOptions: options?.connectionOptions
    }

    const state: ReconnectionState = {
      deviceId,
      options: reconnectionOptions,
      isReconnecting: false,
      retryCount: 0,
      onReconnect: callbacks?.onReconnect,
      onReconnectFailed: callbacks?.onReconnectFailed,
      cancelled: false
    }

    // Subscribe to disconnection events for this device
    state.disconnectSubscription = this._manager.onDeviceDisconnected(deviceId, (error, _device) => {
      // Only auto-reconnect on unexpected disconnections (error is not null)
      // If error is null, it was an intentional disconnect via cancelDeviceConnection
      if (error) {
        this._startReconnection(deviceId)
      }
    })

    this._devices.set(deviceId, state)
  }

  /**
   * Disable automatic reconnection for a device.
   *
   * @param deviceId Device identifier
   * @returns True if auto-reconnect was disabled, false if it wasn't enabled
   */
  disableAutoReconnect(deviceId: DeviceId): boolean {
    const state = this._devices.get(deviceId)
    if (!state) {
      return false
    }

    // Mark as cancelled to stop in-flight reconnection attempts
    state.cancelled = true

    // Clear any pending reconnection attempt
    if (state.timeoutId) {
      clearTimeout(state.timeoutId)
    }

    // Remove disconnect subscription
    if (state.disconnectSubscription) {
      state.disconnectSubscription.remove()
    }

    this._devices.delete(deviceId)
    return true
  }

  /**
   * Disable auto-reconnect for all devices.
   */
  disableAll(): void {
    const deviceIds = Array.from(this._devices.keys())
    for (const deviceId of deviceIds) {
      this.disableAutoReconnect(deviceId)
    }
  }

  /**
   * Check if auto-reconnect is enabled for a device.
   *
   * @param deviceId Device identifier
   * @returns True if auto-reconnect is enabled
   */
  isEnabled(deviceId: DeviceId): boolean {
    return this._devices.has(deviceId)
  }

  /**
   * Check if a device is currently reconnecting.
   *
   * @param deviceId Device identifier
   * @returns True if reconnection is in progress
   */
  isReconnecting(deviceId: DeviceId): boolean {
    const state = this._devices.get(deviceId)
    return state?.isReconnecting ?? false
  }

  /**
   * Get the current retry count for a device.
   *
   * @param deviceId Device identifier
   * @returns Current retry count, or -1 if auto-reconnect is not enabled
   */
  getRetryCount(deviceId: DeviceId): number {
    const state = this._devices.get(deviceId)
    return state?.retryCount ?? -1
  }

  /**
   * Manually trigger a reconnection attempt.
   * Useful if you want to retry after all automatic retries have been exhausted.
   *
   * @param deviceId Device identifier
   * @returns Promise resolving to connected Device
   */
  async manualReconnect(deviceId: DeviceId): Promise<Device> {
    const state = this._devices.get(deviceId)
    if (!state) {
      throw new Error(`Auto-reconnect not enabled for device ${deviceId}`)
    }

    // Reset retry count for manual reconnect
    state.retryCount = 0
    return this._attemptReconnection(deviceId)
  }

  /**
   * Start the reconnection process for a device
   */
  private _startReconnection(deviceId: DeviceId): void {
    const state = this._devices.get(deviceId)
    if (!state || state.isReconnecting) {
      return
    }

    state.isReconnecting = true
    state.retryCount = 0
    this._scheduleReconnection(deviceId, 0)
  }

  /**
   * Schedule a reconnection attempt with delay
   */
  private _scheduleReconnection(deviceId: DeviceId, delay: number): void {
    const state = this._devices.get(deviceId)
    if (!state) {
      return
    }

    state.timeoutId = setTimeout(async () => {
      try {
        await this._attemptReconnection(deviceId)
      } catch (_error) {
        // Error handling is done in _attemptReconnection
      }
    }, delay)
  }

  /**
   * Attempt to reconnect to a device
   */
  private async _attemptReconnection(deviceId: DeviceId): Promise<Device> {
    const state = this._devices.get(deviceId)
    if (!state) {
      throw new Error(`Auto-reconnect not enabled for device ${deviceId}`)
    }

    // Check if cancelled before attempting
    if (state.cancelled) {
      throw new Error(`Auto-reconnect cancelled for device ${deviceId}`)
    }

    state.retryCount++
    const maxRetries = state.options.maxRetries ?? 5

    // Notify about reconnection attempt
    this._globalCallbacks.onReconnecting?.(deviceId, state.retryCount, maxRetries)

    try {
      const device = await this._manager.connectToDevice(deviceId, state.options.connectionOptions)

      // Check if cancelled or removed after await (race condition protection)
      const currentState = this._devices.get(deviceId)
      if (!currentState || currentState !== state || currentState.cancelled) {
        // State was disabled during connection - disconnect immediately
        await this._manager.cancelDeviceConnection(deviceId).catch(() => {
          // Ignore errors when cleaning up
        })
        throw new Error(`Auto-reconnect cancelled for device ${deviceId} during connection`)
      }

      // Success!
      state.isReconnecting = false
      state.retryCount = 0

      // Notify callbacks
      state.onReconnect?.(device)
      this._globalCallbacks.onReconnect?.(device)

      return device
    } catch (error) {
      // Check if cancelled after await
      const currentState = this._devices.get(deviceId)
      if (!currentState || currentState !== state || currentState.cancelled) {
        // Don't schedule retry or call callbacks if cancelled
        throw new Error(`Auto-reconnect cancelled for device ${deviceId}`)
      }

      if (state.retryCount >= maxRetries) {
        // Max retries exceeded
        state.isReconnecting = false
        const bleError = error as BleError

        // Notify callbacks
        state.onReconnectFailed?.(deviceId, bleError)
        this._globalCallbacks.onReconnectFailed?.(deviceId, bleError)

        throw bleError
      }

      // Calculate delay with exponential backoff
      const initialDelay = state.options.initialDelayMs ?? 1000
      const maxDelay = state.options.maxDelayMs ?? 30000
      const multiplier = state.options.backoffMultiplier ?? 2

      const delay = Math.min(initialDelay * Math.pow(multiplier, state.retryCount - 1), maxDelay)

      // Schedule next attempt
      this._scheduleReconnection(deviceId, delay)

      throw error
    }
  }

  /**
   * Destroy the manager and disable all auto-reconnect.
   */
  destroy(): void {
    this.disableAll()
    this._globalCallbacks = {}
  }
}
