//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//

import {
  TxProcessor,
  type BroadcastTargets,
  type Class,
  type Doc,
  type MeasureContext,
  type Ref,
  type SessionData,
  type Tx,
  type TxCUD
} from '@hcengineering/core'
import type {
  BroadcastFunc,
  Middleware,
  MiddlewareCreator,
  PipelineContext,
  TxMiddlewareResult
} from '@hcengineering/server-core'
import { BaseMiddleware, createBroadcastEvent } from '@hcengineering/server-core'

/**
 * @public
 */
export class BroadcastMiddleware extends BaseMiddleware implements Middleware {
  constructor (
    context: PipelineContext,
    protected readonly next: Middleware | undefined,
    readonly broadcast: BroadcastFunc
  ) {
    super(context, next)
    context.broadcastEvent = (ctx, tx) => this.doBroadcast(ctx, tx)
  }

  static create (broadcast: BroadcastFunc): MiddlewareCreator {
    return async (ctx, pipelineContext, next) => new BroadcastMiddleware(pipelineContext, next, broadcast)
  }

  async handleBroadcast (ctx: MeasureContext<SessionData>): Promise<void> {
    await this.next?.handleBroadcast(ctx)

    await this.doBroadcast(ctx, ctx.contextData.broadcast.txes, ctx.contextData.broadcast.targets)
  }

  tx (ctx: MeasureContext<SessionData>, tx: Tx[]): Promise<TxMiddlewareResult> {
    // We collect all broadcast information here, so we could send it later
    ctx.contextData.broadcast.txes.push(...tx)

    return this.provideTx(ctx, tx)
  }

  async doBroadcast (ctx: MeasureContext<SessionData>, tx: Tx[], targets?: BroadcastTargets): Promise<void> {
    if (tx.length === 0) {
      return
    }

    // Combine targets by sender

    const toSendTarget = new Map<string, Tx[]>()

    const getTxes = (key: string): Tx[] => {
      let txes = toSendTarget.get(key)
      if (txes === undefined) {
        txes = [...(toSendTarget.get('') ?? [])] // We also need to add all from to all
        toSendTarget.set(key, txes)
      }
      return txes
    }

    // Put current user as send target
    for (const txd of tx) {
      let target: string[] | undefined
      for (const tt of Object.values(targets ?? {})) {
        target = tt(txd)
        if (target !== undefined) {
          break
        }
      }
      if (target === undefined) {
        getTxes('') // Be sure we have empty one

        // Also add to all other targeted sends
        for (const v of toSendTarget.values()) {
          v.push(txd)
        }
      } else {
        for (const t of target) {
          getTxes(t).push(txd)
        }
      }
    }

    const handleSend = async (
      ctx: MeasureContext<SessionData>,
      derived: Tx[],
      target?: string,
      exclude?: string[]
    ): Promise<void> => {
      if (derived.length === 0) {
        return
      }

      if (derived.length > 10000) {
        await this.sendWithPart(derived, ctx, target, exclude)
      } else {
        // Let's send after our response will go out
        this.broadcast(ctx, derived, target, exclude)
      }
    }

    const toSendAll = toSendTarget.get('') ?? []
    toSendTarget.delete('')

    // Then send targeted and all other
    for (const [k, v] of toSendTarget.entries()) {
      void handleSend(ctx, v, k)
    }
    // Send all other except us.
    await handleSend(ctx, toSendAll, undefined, Array.from(toSendTarget.keys()))
  }

  private async sendWithPart (
    derived: Tx[],
    ctx: MeasureContext<SessionData>,
    target: string | undefined,
    exclude: string[] | undefined
  ): Promise<void> {
    const classes = new Set<Ref<Class<Doc>>>()
    for (const dtx of derived) {
      if (TxProcessor.isExtendsCUD(dtx._class)) {
        classes.add((dtx as TxCUD<Doc>).objectClass)
      }
      const etx = TxProcessor.extractTx(dtx)
      if (TxProcessor.isExtendsCUD(etx._class)) {
        classes.add((etx as TxCUD<Doc>).objectClass)
      }
    }
    const bevent = createBroadcastEvent(Array.from(classes))
    this.broadcast(ctx, [bevent], target, exclude)
  }
}