import type {
  CreateResult,
  FilterType,
  FindOptions,
  PartialResult,
  PhysicalStore,
  WithOptionalId,
} from '@furystack/core'
import { AuthorizationError, selectFields } from '@furystack/core'
import type { Injector } from '@furystack/inject'
import { EventHub, type ListenerErrorPayload } from '@furystack/utils'
import type { DataSetSettings } from './data-set-setting.js'

/**
 * Authorization-enforcing wrapper around a {@link PhysicalStore}. The
 * recommended write gateway for application code — `furystack/no-direct-store-token`
 * enforces this. Each mutation runs the relevant `authorize*` and
 * `modify*` callbacks from {@link DataSetSettings}, persists, then emits
 * `onEntityAdded` / `onEntityUpdated` / `onEntityRemoved` (consumed by
 * entity sync, audit logs).
 *
 * Mutating methods take an `injector` parameter to surface caller identity
 * to the authorizers. For server-side / background work without an HTTP
 * request, wrap the injector with `useSystemIdentityContext` from
 * `@furystack/core`.
 *
 * @example
 * ```ts
 * await usingAsync(
 *   useSystemIdentityContext({ injector, username: 'background-job' }),
 *   async (systemInjector) => {
 *     const dataSet = getDataSetFor(systemInjector, UserDataSet)
 *     await dataSet.add(systemInjector, { username: 'alice', roles: [] })
 *   },
 * )
 * ```
 */
export class DataSet<T, TPrimaryKey extends keyof T, TWritableData = WithOptionalId<T, TPrimaryKey>>
  extends EventHub<{
    onEntityAdded: { injector: Injector; entity: T }
    onEntityUpdated: { injector: Injector; id: T[TPrimaryKey]; change: Partial<T> }
    onEntityRemoved: { injector: Injector; key: T[TPrimaryKey] }
    onListenerError: ListenerErrorPayload
  }>
  implements Disposable
{
  public primaryKey: TPrimaryKey

  public async add(injector: Injector, ...entities: TWritableData[]): Promise<CreateResult<T>> {
    await Promise.all(
      entities.map(async (entity) => {
        if (this.settings.authorizeAdd) {
          const result = await this.settings.authorizeAdd({ injector, entity })
          if (!result.isAllowed) {
            throw new AuthorizationError(result.message)
          }
        }
      }),
    )

    const parsed = await Promise.all(
      entities.map(async (entity) => {
        return this.settings.modifyOnAdd ? await this.settings.modifyOnAdd({ injector, entity }) : entity
      }),
    )

    const createResult = await this.settings.physicalStore.add(...parsed)
    createResult.created.map((entity) => {
      this.emit('onEntityAdded', { injector, entity })
    })
    return createResult
  }

  public async update(injector: Injector, id: T[TPrimaryKey], change: Partial<T>): Promise<void> {
    if (this.settings.authorizeUpdate) {
      const result = await this.settings.authorizeUpdate({ injector, change })
      if (!result.isAllowed) {
        throw new AuthorizationError(result.message)
      }
    }
    if (this.settings.authorizeUpdateEntity) {
      const entity = await this.settings.physicalStore.get(id)
      if (entity) {
        const result = await this.settings.authorizeUpdateEntity({ injector, change, entity })
        if (!result.isAllowed) {
          throw new AuthorizationError(result.message)
        }
      }
    }
    const parsed = this.settings.modifyOnUpdate
      ? await this.settings.modifyOnUpdate({ injector, id, entity: change })
      : change
    await this.settings.physicalStore.update(id, parsed)
    this.emit('onEntityUpdated', { injector, change: parsed, id })
  }

  public async count(injector: Injector, filter?: FilterType<T>): Promise<number> {
    if (this.settings.authorizeGet) {
      const result = await this.settings.authorizeGet({ injector })
      if (!result.isAllowed) {
        throw new AuthorizationError(result.message)
      }
    }
    return await this.settings.physicalStore.count(filter)
  }

  public async find<TFields extends Array<keyof T>>(
    injector: Injector,
    filter: FindOptions<T, TFields>,
  ): Promise<Array<PartialResult<T, TFields>>> {
    if (this.settings.authorizeGet) {
      const result = await this.settings.authorizeGet({ injector })
      if (!result.isAllowed) {
        throw new AuthorizationError(result.message)
      }
    }
    const parsedFilter = this.settings.addFilter ? await this.settings.addFilter({ injector, filter }) : filter
    return this.settings.physicalStore.find(parsedFilter)
  }

  public async get<TSelect extends Array<keyof T>>(
    injector: Injector,
    key: T[TPrimaryKey],
    select?: TSelect,
  ): Promise<PartialResult<T, TSelect> | undefined> {
    if (this.settings.authorizeGet) {
      const result = await this.settings.authorizeGet({ injector })
      if (!result.isAllowed) {
        throw new AuthorizationError(result.message)
      }
    }
    if (this.settings.authorizeGetEntity) {
      const fullEntity = await this.settings.physicalStore.get(key)
      if (!fullEntity) {
        return undefined
      }
      const result = await this.settings.authorizeGetEntity({ injector, entity: fullEntity })
      if (!result.isAllowed) {
        throw new AuthorizationError(result.message)
      }
      if (select) {
        return selectFields(fullEntity as T & object, ...select)
      }
      return fullEntity
    }
    return await this.settings.physicalStore.get(key, select)
  }

  /**
   * Removes by primary key. Pre-load `authorizeRemove` and per-entity
   * `authorizeRemoveEntity` are all-or-nothing — any rejection aborts the
   * whole batch before any persist call. When `authorizeRemoveEntity` is
   * configured, missing keys are silently forwarded to the physical store
   * (no entity to authorize).
   */
  public async remove(injector: Injector, ...keys: Array<T[TPrimaryKey]>): Promise<void> {
    if (keys.length === 0) {
      return
    }
    if (this.settings.authorizeRemove) {
      const result = await this.settings.authorizeRemove({ injector })
      if (!result.isAllowed) {
        throw new AuthorizationError(result.message)
      }
    }
    if (this.settings.authorizeRemoveEntity) {
      const entities = await this.settings.physicalStore.find({
        filter: { [this.primaryKey]: { $in: keys } } as unknown as FilterType<T>,
      })
      await Promise.all(
        entities.map(async (entity) => {
          const removeResult = await this.settings.authorizeRemoveEntity!({ injector, entity })
          if (!removeResult.isAllowed) {
            throw new AuthorizationError(removeResult.message)
          }
        }),
      )
    }
    await this.settings.physicalStore.remove(...keys)
    keys.forEach((key) => this.emit('onEntityRemoved', { injector, key }))
  }

  constructor(public readonly settings: DataSetSettings<T, TPrimaryKey, TWritableData>) {
    super()
    this.primaryKey = this.settings.physicalStore.primaryKey
  }
}
