import {
  LiveQueryWindowControllerDisposedError,
  SetWindowRequiresOrderByError,
} from './errors.js'
import { getLiveQueryStatusFlags } from './live-query-adapter.js'
import { createLiveQueryObserver } from './live-query-observer.js'
import type {
  LiveQueryObserver,
  LiveQuerySnapshot,
} from './live-query-observer.js'
import type { Collection } from './collection/index.js'
import type { CollectionStatus } from './types.js'

const DEFAULT_PAGE_SIZE = 20

type WindowResult = true | Promise<void>

type WindowTarget = object & {
  utils?: {
    setWindow?: (options: { offset: number; limit: number }) => WindowResult
    getWindow?: () => { offset: number; limit: number } | undefined
  }
}

type PendingWindow = {
  generation: number
  limit: number
  promise: Promise<void>
}

class WindowCoordinator {
  private readonly leases = new Map<symbol, number>()
  private readonly leaseVersions = new Map<symbol, number>()
  private readonly initialWindow: { offset: number; limit: number } | undefined
  private appliedLimit: number | undefined
  private pending: PendingWindow | undefined
  private generation = 0
  private leaseVersion = 0

  constructor(private readonly target: WindowTarget) {
    this.initialWindow = target.utils?.getWindow?.()
  }

  request(lease: symbol, limit: number): WindowResult {
    const previousLimit = this.leases.get(lease)
    const previousVersion = this.leaseVersions.get(lease)
    const version = ++this.leaseVersion
    this.leases.set(lease, limit)
    this.leaseVersions.set(lease, version)

    let result: WindowResult
    try {
      result = this.applyDesiredWindow()
    } catch (error) {
      this.rollbackLease(lease, version, previousLimit, previousVersion)
      this.appliedLimit = undefined
      throw error
    }

    if (result === true) return true
    return result.catch(async (error: unknown) => {
      if (this.rollbackLease(lease, version, previousLimit, previousVersion)) {
        this.generation++
        this.pending = undefined
        this.appliedLimit = undefined
        try {
          if (this.leases.size === 0) {
            this.restoreInitialWindow()
          } else {
            const rollback = this.applyDesiredWindow()
            if (rollback !== true) await rollback
          }
        } catch {
          // Preserve the failure from the requested window.
        }
      }
      throw error
    })
  }

  isLeaseSatisfied(lease: symbol, minimumLimit: number): boolean {
    const limit = this.leases.get(lease)
    if (limit === undefined || limit < minimumLimit) return false
    const desiredLimit = this.getDesiredLimit()
    const currentWindow = this.target.utils?.getWindow?.()
    return (
      currentWindow === undefined ||
      (currentWindow.offset === 0 && currentWindow.limit === desiredLimit)
    )
  }

  release(lease: symbol): void {
    if (!this.leases.delete(lease)) return
    this.leaseVersions.delete(lease)

    // A pending request may still mutate the physical operator, but it no longer
    // establishes the accepted window for the remaining lease set.
    this.generation++
    this.pending = undefined
    this.appliedLimit = undefined

    if (this.leases.size === 0) {
      return
    }

    try {
      const result = this.applyDesiredWindow()
      if (result !== true) {
        void result.catch(() => {
          // Unsubscribe has no async error channel. Leave the physical window
          // unaccepted so the next request retries it.
          this.appliedLimit = undefined
        })
      }
    } catch {
      // The remaining controller will retry on its next request.
      this.appliedLimit = undefined
    }
  }

  private getDesiredLimit(): number | undefined {
    let desired: number | undefined
    for (const limit of this.leases.values()) {
      desired = desired === undefined ? limit : Math.max(desired, limit)
    }
    return desired
  }

  private rollbackLease(
    lease: symbol,
    version: number,
    previousLimit: number | undefined,
    previousVersion: number | undefined,
  ): boolean {
    if (this.leaseVersions.get(lease) !== version) return false
    if (previousLimit === undefined) {
      this.leases.delete(lease)
      this.leaseVersions.delete(lease)
    } else {
      this.leases.set(lease, previousLimit)
      if (previousVersion === undefined) {
        this.leaseVersions.delete(lease)
      } else {
        this.leaseVersions.set(lease, previousVersion)
      }
    }
    return true
  }

  private restoreInitialWindow(): void {
    const setWindow = this.target.utils?.setWindow
    if (!this.initialWindow || typeof setWindow !== `function`) return
    try {
      const result = setWindow.call(this.target.utils, this.initialWindow)
      if (result !== true) void result.catch(() => {})
    } catch {
      // Release has no error channel. A future lease will retry its own window.
    }
  }

  private applyDesiredWindow(): WindowResult {
    const limit = this.getDesiredLimit()
    if (limit === undefined) return true
    if (this.pending?.limit === limit) return this.pending.promise
    if (this.pending) {
      // `setWindow` mutates the physical operator before its load promise
      // settles. A different desired window must therefore be applied again,
      // even when it matches the last settled limit.
      this.generation++
      this.pending = undefined
      this.appliedLimit = undefined
    }
    const currentWindow = this.target.utils?.getWindow?.()
    if (
      limit === this.appliedLimit &&
      currentWindow?.offset === 0 &&
      currentWindow.limit === limit
    ) {
      return true
    }

    const setWindow = this.target.utils?.setWindow
    if (typeof setWindow !== `function`) {
      throw new SetWindowRequiresOrderByError()
    }

    const generation = ++this.generation
    const result = setWindow.call(this.target.utils, { offset: 0, limit })
    if (result === true) {
      if (generation === this.generation && this.getDesiredLimit() === limit) {
        this.appliedLimit = limit
      }
      return true
    }

    const promise = result.then(
      () => {
        if (
          generation === this.generation &&
          this.getDesiredLimit() === limit
        ) {
          this.appliedLimit = limit
        }
        if (this.pending?.generation === generation) {
          this.pending = undefined
        }
      },
      (error: unknown) => {
        if (this.pending?.generation === generation) {
          this.pending = undefined
        }
        throw error
      },
    )
    this.pending = { generation, limit, promise }
    return promise
  }
}

const windowCoordinators = new WeakMap<object, WindowCoordinator>()

function getWindowCoordinator(target: WindowTarget): WindowCoordinator {
  let coordinator = windowCoordinators.get(target)
  if (!coordinator) {
    coordinator = new WindowCoordinator(target)
    windowCoordinators.set(target, coordinator)
  }
  return coordinator
}

/**
 * A page-windowed view of a live query at a point in time.
 *
 * @internal This contract is unstable while RFC #1623 is being implemented.
 */
export interface LiveQueryWindowSnapshot<
  T extends object,
  TKey extends string | number,
> {
  /** Rows across all committed pages, with the peek-ahead row removed. */
  data: ReadonlyArray<T>
  /** Rows grouped into committed pages of `pageSize`. */
  pages: ReadonlyArray<ReadonlyArray<T>>
  /** `initialPageParam + i` for each committed page. */
  pageParams: ReadonlyArray<number>
  hasNextPage: boolean
  isFetchingNextPage: boolean
  /** The last pagination failure, cleared when a retry begins. */
  error: unknown
  /** Keyed results for the physical window, or `undefined` when disabled. */
  state: ReadonlyMap<TKey, T> | undefined
  collection: Collection<T, TKey, any> | undefined
  status: CollectionStatus | `disabled`
  isLoading: boolean
  isReady: boolean
  isIdle: boolean
  isError: boolean
  isCleanedUp: boolean
  isEnabled: boolean
}

/** @internal This contract is unstable while RFC #1623 is being implemented. */
export interface CreateLiveQueryWindowControllerOptions {
  /** Rows per page (default 20). Non-positive values use the default. */
  pageSize?: number
  /** Value of the first page's `pageParam` (default 0). */
  initialPageParam?: number
  /** Committed pages to preserve when a framework binding changes page shape. */
  initialPageCount?: number
}

/** @internal This contract is unstable while RFC #1623 is being implemented. */
export interface LiveQueryWindowController<
  T extends object,
  TKey extends string | number,
> {
  getSnapshot: () => LiveQueryWindowSnapshot<T, TKey>
  subscribe: (listener: () => void) => () => void
  /** Load one more page, resolving only after that page is committed. */
  fetchNextPage: () => Promise<void>
  /** Reset to the first page, resolving after the smaller window is accepted. */
  reset: () => Promise<void>
  preload: () => Promise<void>
  dispose: () => void
}

interface CachedFrom {
  observerSnapshot: unknown
  committedPageCount: number
  isFetchingNextPage: boolean
  hasPaginationError: boolean
  paginationError: unknown
  failedHasNextPage: boolean
}

interface SubscriptionRecord {
  listener: () => void
  active: boolean
}

interface Publication {
  targets: Array<SubscriptionRecord>
}

class LiveQueryWindowControllerImpl<
  T extends object,
  TKey extends string | number,
> implements LiveQueryWindowController<T, TKey> {
  private readonly observer: LiveQueryObserver<T, TKey>
  private readonly collection: Collection<T, TKey, any> | null
  private readonly coordinator: WindowCoordinator | null
  private readonly lease = Symbol(`liveQueryWindowLease`)
  private readonly pageSize: number
  private readonly initialPageParam: number

  private committedPageCount: number
  private isFetchingNextPage = false
  private hasPaginationError = false
  private paginationError: unknown
  private failedHasNextPage = false
  private windowGeneration = 0
  private pendingWindowGeneration: number | undefined
  private leaseActive = false
  private leaseGeneration = 0
  private inFlightLeaseHolders = 0

  private readonly subscriptions = new Set<SubscriptionRecord>()
  private readonly publicationQueue: Array<Publication> = []
  private dispatching = false
  private blockDelivery = false
  private transitionDepth = 0
  private transitionNeedsNotify = false
  private observerUnsub: (() => void) | null = null
  private cachedSnapshot: LiveQueryWindowSnapshot<T, TKey> | null = null
  private cachedFrom: CachedFrom | null = null
  private disposed = false

  constructor(
    collection: Collection<T, TKey, any> | null,
    options: CreateLiveQueryWindowControllerOptions,
  ) {
    this.collection = collection
    this.coordinator = collection
      ? getWindowCoordinator(collection as unknown as WindowTarget)
      : null
    this.pageSize =
      options.pageSize !== undefined && options.pageSize > 0
        ? options.pageSize
        : DEFAULT_PAGE_SIZE
    this.initialPageParam = options.initialPageParam ?? 0
    const initialPageCount = Math.floor(options.initialPageCount ?? 1)
    this.committedPageCount = Number.isFinite(initialPageCount)
      ? Math.max(1, initialPageCount)
      : 1
    // The controller listener carries no delta payload, so wholesale is the
    // only coherent observer contract and guarantees non-reentrant subscribe.
    this.observer = createLiveQueryObserver<T, TKey>(collection, {
      mode: `wholesale`,
    })
  }

  getSnapshot(): LiveQueryWindowSnapshot<T, TKey> {
    const observerSnapshot = this.observer.getSnapshot()
    const cached = this.cachedSnapshot
    if (
      cached &&
      this.cachedFrom &&
      this.cachedFrom.observerSnapshot === observerSnapshot &&
      this.cachedFrom.committedPageCount === this.committedPageCount &&
      this.cachedFrom.isFetchingNextPage === this.isFetchingNextPage &&
      this.cachedFrom.hasPaginationError === this.hasPaginationError &&
      this.cachedFrom.paginationError === this.paginationError &&
      this.cachedFrom.failedHasNextPage === this.failedHasNextPage
    ) {
      return cached
    }

    const enabled = observerSnapshot.isEnabled
    const rows =
      enabled && Array.isArray(observerSnapshot.data)
        ? (observerSnapshot.data as ReadonlyArray<T>)
        : []
    const totalRequested = this.committedPageCount * this.pageSize
    const computedHasNextPage = enabled && rows.length > totalRequested
    const hasNextPage = this.hasPaginationError
      ? this.failedHasNextPage
      : computedHasNextPage

    const pageCount = enabled ? this.committedPageCount : 0
    const pages: Array<ReadonlyArray<T>> = []
    const pageParams: Array<number> = []
    for (let i = 0; i < pageCount; i++) {
      pages.push(rows.slice(i * this.pageSize, (i + 1) * this.pageSize))
      pageParams.push(this.initialPageParam + i)
    }

    const status = this.hasPaginationError ? `error` : observerSnapshot.status
    const statusFlags = this.hasPaginationError
      ? getLiveQueryStatusFlags(`error`)
      : observerSnapshot
    this.cachedSnapshot = {
      data: rows.slice(0, totalRequested),
      pages,
      pageParams,
      hasNextPage,
      isFetchingNextPage: this.isFetchingNextPage,
      error: this.hasPaginationError ? this.paginationError : undefined,
      state: observerSnapshot.state,
      collection: observerSnapshot.collection,
      status,
      isLoading: statusFlags.isLoading,
      isReady: statusFlags.isReady,
      isIdle: statusFlags.isIdle,
      isError: statusFlags.isError,
      isCleanedUp: observerSnapshot.isCleanedUp,
      isEnabled: observerSnapshot.isEnabled,
    }
    this.cachedFrom = {
      observerSnapshot,
      committedPageCount: this.committedPageCount,
      isFetchingNextPage: this.isFetchingNextPage,
      hasPaginationError: this.hasPaginationError,
      paginationError: this.paginationError,
      failedHasNextPage: this.failedHasNextPage,
    }
    return this.cachedSnapshot
  }

  subscribe(listener: () => void): () => void {
    if (this.disposed) throw new LiveQueryWindowControllerDisposedError()

    const record: SubscriptionRecord = { listener, active: true }
    this.subscriptions.add(record)
    if (this.subscriptions.size === 1) {
      this.blockDelivery = true
      let observerUnsub: (() => void) | null = null
      try {
        // Store the desired physical window before observer activation can
        // compile or restart the live-query pipeline.
        const windowResult = this.ensureLeaseActive(this.committedPageCount)
        const leaseGeneration = this.leaseGeneration
        observerUnsub = this.observer.subscribe(() => this.onObserverNotify())
        this.observerUnsub = observerUnsub
        if (windowResult !== true) {
          this.trackAttachmentFailure(windowResult, leaseGeneration)
        }
      } catch (error) {
        observerUnsub?.()
        this.observerUnsub = null
        this.deactivateLease()
        record.active = false
        this.subscriptions.delete(record)
        throw error
      } finally {
        this.blockDelivery = false
      }
    }

    return () => {
      if (!record.active) return
      record.active = false
      this.subscriptions.delete(record)
      if (this.subscriptions.size === 0) {
        this.observerUnsub?.()
        this.observerUnsub = null
        if (this.inFlightLeaseHolders === 0) this.deactivateLease()
      }
    }
  }

  fetchNextPage(): Promise<void> {
    if (this.disposed || this.isFetchingNextPage) return Promise.resolve()
    if (!this.getSnapshot().hasNextPage) return Promise.resolve()
    return this.requestPageCount(this.committedPageCount + 1, true)
  }

  reset(): Promise<void> {
    if (this.disposed) return Promise.resolve()
    if (
      this.committedPageCount === 1 &&
      !this.hasPaginationError &&
      !this.isFetchingNextPage &&
      this.pendingWindowGeneration === undefined
    ) {
      return Promise.resolve()
    }
    return this.requestPageCount(1, false)
  }

  async preload(): Promise<void> {
    if (this.disposed) throw new LiveQueryWindowControllerDisposedError()

    const hadPaginationError = this.hasPaginationError
    this.hasPaginationError = false
    this.paginationError = undefined
    this.acquireInFlightLease()
    try {
      const result = this.ensureLeaseActive(this.committedPageCount)
      if (result !== true) await result
      await this.observer.preload()
      this.failedHasNextPage = false
      if (hadPaginationError) this.notify()
    } catch (error) {
      this.hasPaginationError = true
      this.paginationError = error
      this.failedHasNextPage = this.getComputedHasNextPage()
      this.notify()
      throw error
    } finally {
      this.releaseInFlightLease()
    }
  }

  dispose(): void {
    if (this.disposed) return
    this.disposed = true
    this.windowGeneration++
    this.pendingWindowGeneration = undefined
    this.observerUnsub?.()
    this.observerUnsub = null
    this.deactivateLease()
    this.observer.dispose()
    for (const record of this.subscriptions) record.active = false
    this.subscriptions.clear()
    this.publicationQueue.length = 0
  }

  private requestPageCount(
    requestedPageCount: number,
    fetchingNextPage: boolean,
  ): Promise<void> {
    const generation = ++this.windowGeneration
    const previousHasNextPage = this.getSnapshot().hasNextPage
    this.pendingWindowGeneration = undefined
    this.acquireInFlightLease()

    this.beginTransition()
    this.isFetchingNextPage = fetchingNextPage
    this.hasPaginationError = false
    this.paginationError = undefined
    if (fetchingNextPage) this.notify()

    let result: WindowResult
    try {
      result = this.activateLease(requestedPageCount)
    } catch (error) {
      this.isFetchingNextPage = false
      this.hasPaginationError = true
      this.paginationError = error
      this.failedHasNextPage = previousHasNextPage
      this.notify()
      this.endTransition()
      this.releaseInFlightLease()
      return Promise.reject(error)
    }

    if (result === true) {
      if (!this.disposed && generation === this.windowGeneration) {
        this.committedPageCount = requestedPageCount
        this.isFetchingNextPage = false
        this.failedHasNextPage = false
        this.notify()
      }
      this.endTransition()
      this.releaseInFlightLease()
      return Promise.resolve()
    }

    this.pendingWindowGeneration = generation
    this.endTransition()

    return result
      .then(
        () => {
          if (this.disposed || generation !== this.windowGeneration) return
          this.beginTransition()
          this.pendingWindowGeneration = undefined
          this.committedPageCount = requestedPageCount
          this.isFetchingNextPage = false
          this.failedHasNextPage = false
          this.notify()
          this.endTransition()
        },
        (error: unknown) => {
          if (!this.disposed && generation === this.windowGeneration) {
            this.beginTransition()
            this.pendingWindowGeneration = undefined
            this.isFetchingNextPage = false
            this.hasPaginationError = true
            this.paginationError = error
            this.failedHasNextPage = previousHasNextPage
            this.notify()
            this.endTransition()
          }
          throw error
        },
      )
      .finally(() => {
        this.releaseInFlightLease()
      })
  }

  private acquireInFlightLease(): void {
    this.inFlightLeaseHolders++
  }

  private releaseInFlightLease(): void {
    this.inFlightLeaseHolders--
    if (this.inFlightLeaseHolders === 0 && this.subscriptions.size === 0) {
      this.deactivateLease()
    }
  }

  private activateLease(pageCount: number): WindowResult {
    this.leaseGeneration++
    if (!this.coordinator || !this.collection) return true
    this.leaseActive = true
    return this.coordinator.request(this.lease, pageCount * this.pageSize + 1)
  }

  private ensureLeaseActive(pageCount: number): WindowResult {
    const minimumLimit = pageCount * this.pageSize + 1
    if (
      this.leaseActive &&
      this.coordinator?.isLeaseSatisfied(this.lease, minimumLimit)
    ) {
      return true
    }
    return this.activateLease(pageCount)
  }

  private deactivateLease(): void {
    if (!this.leaseActive || !this.coordinator) return
    this.leaseGeneration++
    this.leaseActive = false
    this.coordinator.release(this.lease)
  }

  private trackAttachmentFailure(
    result: Promise<void>,
    leaseGeneration: number,
  ): void {
    void result.catch((error: unknown) => {
      if (
        this.disposed ||
        !this.leaseActive ||
        leaseGeneration !== this.leaseGeneration
      ) {
        return
      }
      this.beginTransition()
      this.hasPaginationError = true
      this.paginationError = error
      this.failedHasNextPage = this.getComputedHasNextPage()
      this.notify()
      this.endTransition()
    })
  }

  private getComputedHasNextPage(): boolean {
    const snapshot: LiveQuerySnapshot<T, TKey> = this.observer.getSnapshot()
    return (
      snapshot.isEnabled &&
      Array.isArray(snapshot.data) &&
      snapshot.data.length > this.committedPageCount * this.pageSize
    )
  }

  private onObserverNotify(): void {
    this.notify()
  }

  private beginTransition(): void {
    this.transitionDepth++
  }

  private endTransition(): void {
    this.transitionDepth--
    if (this.transitionDepth === 0 && this.transitionNeedsNotify) {
      this.transitionNeedsNotify = false
      this.publish()
    }
  }

  private notify(): void {
    if (this.transitionDepth > 0) {
      this.transitionNeedsNotify = true
      return
    }
    this.publish()
  }

  private publish(): void {
    if (this.disposed || this.blockDelivery || this.subscriptions.size === 0) {
      return
    }

    this.publicationQueue.push({ targets: [...this.subscriptions] })
    if (this.dispatching) return

    this.dispatching = true
    try {
      while (this.publicationQueue.length > 0) {
        const publication = this.publicationQueue.shift()!
        for (const record of publication.targets) {
          if (this.hasBeenDisposed()) return
          if (!record.active) continue
          record.listener()
        }
      }
    } finally {
      this.dispatching = false
    }
  }

  private hasBeenDisposed(): boolean {
    return this.disposed
  }
}

/**
 * Create an internal forward-window controller for an ordered live query.
 *
 * @internal This factory is unstable while RFC #1623 is being implemented.
 */
export function createLiveQueryWindowController<
  T extends object,
  TKey extends string | number,
>(
  collection: Collection<T, TKey, any> | null | undefined,
  options: CreateLiveQueryWindowControllerOptions = {},
): LiveQueryWindowController<T, TKey> {
  return new LiveQueryWindowControllerImpl<T, TKey>(collection ?? null, options)
}
