//
// Copyright © 2023 Hardcore Engineering Inc.
//
//

import chunter from '@hcengineering/chunter'
import core, {
  Account,
  BrandingMap,
  Client,
  ClientConnectEvent,
  DocumentUpdate,
  MeasureContext,
  RateLimiter,
  Ref,
  TxOperations
} from '@hcengineering/core'
import github, { GithubAuthentication, makeQuery, type GithubIntegration } from '@hcengineering/github'
import { getMongoClient, MongoClientReference } from '@hcengineering/mongo'
import { setMetadata } from '@hcengineering/platform'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import serverToken, { generateToken } from '@hcengineering/server-token'
import tracker from '@hcengineering/tracker'
import { Installation, type InstallationCreatedEvent, type InstallationUnsuspendEvent } from '@octokit/webhooks-types'
import { Collection } from 'mongodb'
import { App, Octokit } from 'octokit'

import { ClientWorkspaceInfo } from '@hcengineering/account'
import { Analytics } from '@hcengineering/analytics'
import { SplitLogger } from '@hcengineering/analytics-service'
import contact, { Person, PersonAccount } from '@hcengineering/contact'
import { type StorageAdapter } from '@hcengineering/server-core'
import { join } from 'path'
import { getWorkspaceInfo } from './account'
import { createPlatformClient } from './client'
import config from './config'
import { registerLoaders } from './loaders'
import { createNotification } from './notifications'
import { errorToObj } from './sync/utils'
import { GithubIntegrationRecord, GithubUserRecord } from './types'
import { UserManager } from './users'
import { GithubWorker, syncUser } from './worker'

export interface InstallationRecord {
  installationName: string
  login: string
  loginNodeId: string

  repositories?: InstallationCreatedEvent['repositories'] | InstallationUnsuspendEvent['repositories']
  type: 'Bot' | 'User' | 'Organization'
  octokit: Octokit
}

export class PlatformWorker {
  private readonly clients: Map<string, GithubWorker> = new Map<string, GithubWorker>()

  storageAdapter!: StorageAdapter

  installations = new Map<number, InstallationRecord>()

  integrations: GithubIntegrationRecord[] = []

  mongoRef!: MongoClientReference

  integrationCollection!: Collection<GithubIntegrationRecord>

  periodicTimer: any
  periodicSyncPromise: Promise<void> | undefined

  canceled = false

  userManager!: UserManager

  private constructor (
    readonly ctx: MeasureContext,
    readonly app: App,
    readonly brandingMap: BrandingMap,
    readonly periodicSyncInterval = 10 * 60 * 1000 // 10 minutes
  ) {
    setMetadata(serverToken.metadata.Secret, config.ServerSecret)
    registerLoaders()
  }

  public async initStorage (): Promise<void> {
    this.mongoRef = getMongoClient(config.MongoURL)
    const mongoClient = await this.mongoRef.getClient()

    const db = mongoClient.db(config.ConfigurationDB)
    this.integrationCollection = db.collection<GithubIntegrationRecord>('installations')

    this.userManager = new UserManager(db.collection<GithubUserRecord>('users'))

    const storageConfig = storageConfigFromEnv()
    this.storageAdapter = buildStorageFromConfig(storageConfig, config.MongoURL)
  }

  async close (): Promise<void> {
    this.canceled = true
    await Promise.all(
      [...this.clients.values()].map(async (worker) => {
        await worker.close()
      })
    )
    this.clients.clear()
    await this.storageAdapter.close()
    this.mongoRef.close()
  }

  async init (ctx: MeasureContext): Promise<void> {
    this.integrations = await this.integrationCollection.find({}).toArray()
    await this.queryInstallations(ctx)

    for (const integr of [...this.integrations]) {
      // We need to check and remove integrations without a real integration's
      if (!this.installations.has(integr.installationId)) {
        ctx.warn('Installation was deleted during service shutdown', {
          installationId: integr.installationId,
          workspace: integr.workspace
        })
        await this.integrationCollection.deleteOne({ installationId: integr.installationId })
        this.integrations = this.integrations.filter((it) => it.installationId !== integr.installationId)
      }
    }

    void this.doSyncWorkspaces().catch((err) => {
      ctx.error('error during sync workspaces', { err })
      process.exit(1)
    })

    this.periodicTimer = setInterval(() => {
      if (this.periodicSyncPromise === undefined) {
        this.periodicSyncPromise = this.performPeriodicSync()
      }
    }, this.periodicSyncInterval)
  }

  async performPeriodicSync (): Promise<void> {
    // Sync authorized users information details.
    const workspaces = await this.findUsersWorkspaces()
    for (const [workspace, users] of workspaces) {
      const worker = this.clients.get(workspace)
      if (worker !== undefined) {
        await this.ctx.with('syncUsers', {}, async (ctx) => {
          await worker.syncUserData(ctx, users)
        })
      }
    }
    this.periodicSyncPromise = undefined
  }

  triggerCheckWorkspaces = (): void => {}

  async doSyncWorkspaces (): Promise<void> {
    while (!this.canceled) {
      let errors = false
      try {
        errors = await this.checkWorkspaces()
      } catch (err: any) {
        Analytics.handleError(err)
        this.ctx.error('check workspace', err)
        errors = true
      }
      await new Promise<void>((resolve) => {
        this.triggerCheckWorkspaces = resolve
        this.ctx.info('Workspaces check triggered')
        if (errors) {
          setTimeout(resolve, 5000)
        }
      })
    }
  }

  private async findUsersWorkspaces (): Promise<Map<string, GithubUserRecord[]>> {
    const i = this.userManager.getAllUsers()
    const workspaces = new Map<string, GithubUserRecord[]>()
    while (await i.hasNext()) {
      const userInfo = await i.next()
      if (userInfo !== null) {
        for (const ws of Object.keys(userInfo.accounts ?? {})) {
          if (this.integrations.find((it) => it.workspace === ws) === undefined) {
            // No workspace integration found, let's check workspace.
            workspaces.set(ws, [...(workspaces.get(ws) ?? []), userInfo])
          }
        }
      }
    }
    await i.close()
    return workspaces
  }

  public async getUsers (workspace: string): Promise<GithubUserRecord[]> {
    return await this.userManager.getUsers(workspace)
  }

  public async getUser (login: string): Promise<GithubUserRecord | undefined> {
    return await this.userManager.getAccount(login)
  }

  async mapInstallation (
    ctx: MeasureContext,
    workspace: string,
    installationId: number,
    accountId: Ref<Account>
  ): Promise<void> {
    const oldInstallation = this.integrations.find((it) => it.installationId === installationId)
    if (oldInstallation != null) {
      ctx.info('update integration', { workspace, installationId, accountId })
      // What to do with installation in different workspace?
      // Let's remove it and sync to new one.
      if (oldInstallation.workspace !== workspace) {
        //
        const oldWorkspace = oldInstallation.workspace

        await this.integrationCollection.updateOne(
          { installationId: oldInstallation.installationId },
          { $set: { workspace } }
        )
        oldInstallation.workspace = workspace

        const oldWorker = this.clients.get(oldWorkspace) as GithubWorker
        if (oldWorker !== undefined) {
          await this.removeInstallationFromWorkspace(oldWorker.client, installationId)
          await oldWorker.reloadRepositories(installationId)
        } else {
          let client: Client | undefined
          try {
            client = await createPlatformClient(oldWorkspace, 30000)
            await this.removeInstallationFromWorkspace(oldWorker, installationId)
            await client.close()
          } catch (err: any) {
            ctx.error('failed to remove old installation from workspace', { workspace: oldWorkspace, installationId })
          }
        }
      }

      await this.updateInstallation(installationId)

      const worker = this.clients.get(workspace) as GithubWorker
      await worker?.reloadRepositories(installationId)
      worker?.triggerUpdate()

      this.triggerCheckWorkspaces()
      return
    }
    const record: GithubIntegrationRecord = {
      workspace,
      installationId,
      accountId
    }
    ctx.info('add integration', { workspace, installationId, accountId })

    await ctx.with('add integration', { workspace, installationId, accountId }, async (ctx) => {
      await this.integrationCollection.insertOne(record)
      this.integrations.push(record)
    })
    // We need to query installations to be sure we have it, in case event is delayed or not received.
    await this.updateInstallation(installationId)

    const worker = this.clients.get(workspace) as GithubWorker
    await worker?.reloadRepositories(installationId)
    worker?.triggerUpdate()

    this.triggerCheckWorkspaces()
  }

  private async removeInstallationFromWorkspace (client: Client, installationId: number): Promise<void> {
    const wsIntegerations = await client.findAll(github.class.GithubIntegration, { installationId })

    const ops = new TxOperations(client, core.account.System)
    for (const intValue of wsIntegerations) {
      await ops.remove<GithubIntegration>(intValue)
    }
  }

  async removeInstallation (ctx: MeasureContext, workspace: string, installationId: number): Promise<void> {
    const installation = this.installations.get(installationId)
    if (installation !== undefined) {
      try {
        await installation.octokit.rest.apps.deleteInstallation({
          installation_id: installationId
        })
      } catch (err: any) {
        if (err.status !== 404) {
          // Already deleted.
          ctx.error('error from github api', { error: err })
        }
        await this.handleInstallationEventDelete(installationId)
      }
      // Let's check if workspace somehow still have installation and remove it
      const worker = this.clients.get(workspace) as GithubWorker
      if (worker !== undefined) {
        await GithubWorker.checkIntegrations(worker.client, this.installations)
      } else {
        let client: Client | undefined
        try {
          client = await createPlatformClient(workspace, 30000)
          await GithubWorker.checkIntegrations(client, this.installations)
          await client.close()
        } catch (err: any) {
          ctx.error('failed to clean installation from workspace', { workspace, installationId })
        }
      }
    }
    this.triggerCheckWorkspaces()
  }

  async requestGithubAccessToken (payload: {
    workspace: string
    code: string
    state: string
    accountId: Ref<Account>
  }): Promise<void> {
    try {
      const uri =
        'https://github.com/login/oauth/access_token?' +
        makeQuery({
          client_id: config.ClientID,
          client_secret: config.ClientSecret,
          code: payload.code,
          state: payload.state
        })
      const result = await fetch(uri, {
        method: 'POST',
        headers: {
          Accept: 'application/json'
        }
      })

      const resultJson = await result.json()
      if (resultJson.error !== undefined) {
        await this.updateAccountAuthRecord(payload, { error: null }, undefined, false)
      } else {
        const okit = new Octokit({
          auth: resultJson.access_token,
          client_id: config.ClientID,
          client_secret: config.ClientSecret
        })
        const user = await okit.rest.users.getAuthenticated()
        const nowTime = Date.now() / 1000
        const dta: GithubUserRecord = {
          _id: user.data.login,
          token: resultJson.access_token,
          code: null,
          expiresIn: resultJson.expires_in != null ? nowTime + (resultJson.expires_in as number) : null,
          refreshToken: resultJson.refresh_token ?? null,
          refreshTokenExpiresIn:
            resultJson.refresh_token_expires_in !== undefined
              ? nowTime + (resultJson.refresh_token_expires_in as number)
              : null,
          scope: resultJson.scope,
          accounts: { [payload.workspace]: payload.accountId }
        }
        await this.userManager.updateUser(dta)
        const existingUser = await this.userManager.getAccount(user.data.login)
        if (existingUser == null) {
          await this.userManager.insertUser(dta)
        } else {
          dta.accounts = { ...existingUser.accounts, [payload.workspace]: payload.accountId }
          await this.userManager.updateUser(dta)
        }

        // Update workspace client login info.
        await this.updateAccountAuthRecord(
          payload,
          {
            login: dta._id,
            error: null,
            avatar: user.data.avatar_url,
            name: user.data.name ?? '',
            url: user.data.url
          },
          dta,
          false
        )
      }
    } catch (err: any) {
      Analytics.handleError(err)
      await this.updateAccountAuthRecord(payload, { error: errorToObj(err) }, undefined, false)
    }
  }

  private async updateAccountAuthRecord (
    payload: { workspace: string, accountId: Ref<Account> },
    update: DocumentUpdate<GithubAuthentication>,
    dta: GithubUserRecord | undefined,
    revoke: boolean
  ): Promise<void> {
    try {
      let platformClient: Client | undefined
      let shouldClose = false
      try {
        platformClient = this.clients.get(payload.workspace)?.client
        if (platformClient === undefined) {
          shouldClose = true
          platformClient = await createPlatformClient(payload.workspace, 30000)
        }
        const client = new TxOperations(platformClient, payload.accountId)

        const personAuth = await client.findOne(github.class.GithubAuthentication, {
          attachedTo: payload.accountId
        })
        if (personAuth !== undefined) {
          if (revoke) {
            await client.remove(personAuth, Date.now(), payload.accountId)
          } else {
            await client.update<GithubAuthentication>(personAuth, update, false, Date.now(), payload.accountId)
          }
        }

        // We need to re-bind previously created github:login account to a proper person.
        const account = client.getModel().getObject(payload.accountId) as PersonAccount
        const person = (await client.findOne(contact.class.Person, { _id: account.person })) as Person
        if (person !== undefined) {
          if (!revoke) {
            const personSpace = await client.findOne(contact.class.PersonSpace, { person: person._id })
            if (personSpace !== undefined) {
              await createNotification(client, person, {
                user: account._id,
                space: personSpace._id,
                message: github.string.AuthenticatedWithGithub,
                props: {
                  login: update.login
                }
              })
            }

            const githubAccount = client.getModel().getAccountByEmail('github:' + update.login) as PersonAccount
            if (githubAccount !== undefined && githubAccount.person !== account.person) {
              const dummyPerson = githubAccount.person
              // To add activity entry to dummy person.
              await client.update(githubAccount, { person: account.person }, false, Date.now(), payload.accountId)

              const dPerson = (await client.findOne(contact.class.Person, { _id: dummyPerson })) as Person
              if (person !== undefined && dPerson !== undefined) {
                const personSpace = await client.findOne(contact.class.PersonSpace, { person: person._id })
                if (personSpace !== undefined) {
                  await createNotification(client, dPerson, {
                    user: githubAccount._id,
                    space: personSpace._id,
                    message: github.string.AuthenticatedWithGithubEmployee,
                    props: {
                      login: update.login
                    }
                  })
                }
              }
            }
          } else {
            const personSpace = await client.findOne(contact.class.PersonSpace, { person: person._id })
            if (personSpace !== undefined) {
              await createNotification(client, person, {
                user: account._id,
                space: personSpace._id,
                message: github.string.AuthenticationRevokedGithub,
                props: {
                  login: update.login
                }
              })
            }
          }
        }

        if (dta !== undefined && personAuth !== undefined) {
          try {
            await syncUser(this.ctx, dta, personAuth, client, payload.accountId)
          } catch (err: any) {
            if (err.response?.data?.message === 'Bad credentials') {
              await this.revokeUserAuth(dta)
            } else {
              this.ctx.error(`Failed to sync user ${dta._id}`, { error: errorToObj(err) })
            }
          }
        }
      } finally {
        if (shouldClose) {
          await platformClient?.close()
        }
      }
    } catch (err: any) {
      Analytics.handleError(err)
    }
  }

  async checkRefreshToken (auth: GithubUserRecord, force: boolean = false): Promise<void> {
    if (auth.refreshToken != null && auth.expiresIn != null && auth.expiresIn < Date.now() / 1000) {
      const uri =
        'https://github.com/login/oauth/access_token?' +
        makeQuery({
          client_id: config.ClientID,
          client_secret: config.ClientSecret,
          grant_type: 'refresh_token',
          refresh_token: auth.refreshToken
        })

      const result = await fetch(uri, {
        method: 'POST',
        headers: {
          Accept: 'application/json'
        }
      })
      const resultJson = await result.json()

      if (resultJson.error !== undefined) {
        // We need to clear github integration info.
        await this.revokeUserAuth(auth)
      } else {
        // Update okit
        const nowTime = Date.now() / 1000
        const dta: GithubUserRecord = {
          ...auth,
          token: resultJson.access_token,
          code: null,
          expiresIn: nowTime + (resultJson.expires_in as number),
          refreshToken: resultJson.refresh_token,
          refreshTokenExpiresIn: nowTime + (resultJson.refresh_token_expires_in as number),
          scope: resultJson.scope
        }
        auth.token = resultJson.access_token
        auth.code = null
        auth.expiresIn = dta.expiresIn
        auth.refreshToken = dta.refreshToken
        auth.refreshTokenExpiresIn = dta.refreshTokenExpiresIn
        auth.scope = dta.scope

        await this.userManager.updateUser(dta)
      }
    }
  }

  async getAccount (login: string): Promise<GithubUserRecord | undefined> {
    return await this.userManager.getAccount(login)
  }

  async getAccountByRef (workspace: string, ref: Ref<Account>): Promise<GithubUserRecord | undefined> {
    return await this.userManager.getAccountByRef(workspace, ref)
  }

  private async updateInstallation (installationId: number): Promise<void> {
    const install = await this.app.octokit.rest.apps.getInstallation({ installation_id: installationId })
    if (install !== null) {
      const tinst = install.data as Installation
      const val: InstallationRecord = {
        octokit: await this.app.getInstallationOctokit(installationId),
        login: tinst.account.login,
        loginNodeId: tinst.account.node_id,
        type: tinst.account?.type ?? 'User',
        installationName: `${tinst.account?.html_url ?? ''}`
      }
      this.updateInstallationRecord(installationId, val)
    }
  }

  private updateInstallationRecord (installationId: number, val: InstallationRecord): void {
    const current = this.installations.get(installationId)
    if (current !== undefined) {
      if (val.octokit !== undefined) {
        current.octokit = val.octokit
      }
      current.login = val.login
      current.loginNodeId = val.loginNodeId
      current.type = val.type
      current.installationName = val.installationName
      if (val.repositories !== undefined) {
        current.repositories = val.repositories
      }
    } else {
      this.installations.set(installationId, val)
    }
  }

  private async queryInstallations (ctx: MeasureContext): Promise<void> {
    for await (const install of this.app.eachInstallation.iterator()) {
      const tinst = install.installation as Installation
      const val: InstallationRecord = {
        octokit: install.octokit,
        login: tinst.account.login,
        loginNodeId: tinst.account.node_id,
        type: tinst.account?.type ?? 'User',
        installationName: `${tinst.account?.html_url ?? ''}`
      }
      this.updateInstallationRecord(install.installation.id, val)
      ctx.info('Found installation', {
        installationId: install.installation.id,
        url: install.installation.account?.html_url ?? ''
      })
    }
  }

  async handleInstallationEvent (
    install: Installation,
    repositories: InstallationCreatedEvent['repositories'] | InstallationUnsuspendEvent['repositories'],
    enabled: boolean
  ): Promise<void> {
    this.ctx.info('handle integration add', { installId: install.id, name: install.html_url })
    const okit = await this.app.getInstallationOctokit(install.id)
    const iName = `${install.account.html_url ?? ''}`

    this.updateInstallationRecord(install.id, {
      octokit: okit,
      login: install.account.login,
      type: install.account?.type ?? 'User',
      loginNodeId: install.account.node_id,
      installationName: iName,
      repositories
    })

    const worker = this.getWorker(install.id)
    if (worker !== undefined) {
      await worker.syncUserData(this.ctx, await this.getUsers(worker.workspace.name))
      await worker.reloadRepositories(install.id)

      worker.triggerUpdate()
      worker.triggerSync()
    }

    // Need to inform workspace
    const integeration = this.integrations.find((it) => it.installationId === install.id)
    if (integeration !== undefined) {
      const worker = this.clients.get(integeration.workspace) as GithubWorker
      worker?.triggerUpdate()
    }

    // Check if no workspace was available
    this.triggerCheckWorkspaces()
  }

  async handleInstallationEventDelete (installId: number): Promise<void> {
    const existing = this.installations.get(installId)
    this.installations.delete(installId)
    this.ctx.info('handle integration delete', { installId, name: existing?.installationName })

    const interg = this.integrations.find((it) => it.installationId === installId)

    // We already have, worker we need to update it.
    const worker = this.getWorker(installId) ?? (interg !== undefined ? this.clients.get(interg.workspace) : undefined)
    if (worker !== undefined) {
      const integeration = worker.integrations.get(installId)
      if (integeration !== undefined) {
        integeration.enabled = false
        integeration.synchronized = new Set()

        await this.removeInstallationFromWorkspace(worker._client, installId)

        await worker._client.remove(integeration.integration)
      }
      worker.integrations.delete(installId)
      worker.triggerUpdate()
    } else {
      this.ctx.info('No worker for removed installation', { installId, name: existing?.installationName })
      // No worker
    }
    this.integrations = this.integrations.filter((it) => it.installationId !== installId)
    await this.integrationCollection.deleteOne({ installationId: installId })
    this.triggerCheckWorkspaces()
  }

  async getWorkspaces (): Promise<string[]> {
    const workspaces = new Set(this.integrations.map((it) => it.workspace))

    return Array.from(workspaces)
  }

  private async checkWorkspaces (): Promise<boolean> {
    let workspaces = await this.getWorkspaces()
    if (process.env.GITHUB_USE_WS !== undefined) {
      workspaces = [process.env.GITHUB_USE_WS]
    }
    const toDelete = new Set<string>(this.clients.keys())

    const rateLimiter = new RateLimiter(5)
    let errors = 0
    let idx = 0
    const connecting = new Map<string, number>()
    const connectingInfo = setInterval(() => {
      for (const [c, d] of connecting.entries()) {
        this.ctx.info('connecting to workspace', { workspace: c, time: Date.now() - d })
      }
    }, 5000)
    for (const workspace of workspaces) {
      const widx = ++idx
      if (this.clients.has(workspace)) {
        toDelete.delete(workspace)
        continue
      }
      await rateLimiter.add(async () => {
        const token = generateToken(
          config.SystemEmail,
          {
            name: workspace
          },
          { mode: 'github' }
        )
        let workspaceInfo: ClientWorkspaceInfo | undefined
        try {
          workspaceInfo = await getWorkspaceInfo(token)
        } catch (err: any) {
          this.ctx.error('Workspace not found:', { workspace })
          errors++
          return
        }
        if (workspaceInfo?.workspace === undefined) {
          this.ctx.error('No workspace exists for workspaceId', { workspace })
          errors++
          return
        }
        if (workspaceInfo?.disabled === true) {
          this.ctx.error('Workspace is disabled workspaceId', { workspace })
          return
        }
        try {
          const branding = Object.values(this.brandingMap).find((b) => b.key === workspaceInfo?.branding) ?? null
          const workerCtx = this.ctx.newChild('worker', { workspace: workspaceInfo.workspace }, {})

          connecting.set(workspaceInfo.workspace, Date.now())
          workerCtx.info('************************* Register worker ************************* ', {
            workspaceId: workspaceInfo.workspaceId,
            workspace: workspaceInfo.workspace,
            index: widx,
            total: workspaces.length
          })

          const worker = await GithubWorker.create(
            this,
            workerCtx,
            this.installations,
            {
              name: workspace,
              workspaceUrl: workspaceInfo.workspace,
              workspaceName: workspace
            },
            branding,
            this.app,
            this.storageAdapter,
            (workspace, event) => {
              if (event === ClientConnectEvent.Refresh || event === ClientConnectEvent.Upgraded) {
                void this.clients.get(workspace)?.refreshClient(event === ClientConnectEvent.Upgraded)
              }
            }
          )
          if (worker !== undefined) {
            workerCtx.info('Register worker Done', {
              workspaceId: workspaceInfo.workspaceId,
              workspace: workspaceInfo.workspace,
              index: widx,
              total: workspaces.length
            })
            // No if no integration, we will try connect one more time in a time period
            this.clients.set(workspace, worker)
          } else {
            workerCtx.info('Failed Register worker, timeout or integrations removed', {
              workspaceId: workspaceInfo.workspaceId,
              workspace: workspaceInfo.workspace,
              index: widx,
              total: workspaces.length
            })
            errors++
          }
        } catch (e: any) {
          Analytics.handleError(e)
          this.ctx.info("Couldn't create WS worker", { workspace, error: e })
          console.error(e)
          errors++
        } finally {
          connecting.delete(workspaceInfo.workspace)
        }
      })
    }
    try {
      await rateLimiter.waitProcessing()
    } catch (e: any) {
      Analytics.handleError(e)
      errors++
    }
    clearInterval(connectingInfo)
    // Close deleted workspaces
    for (const deleted of Array.from(toDelete.keys())) {
      const ws = this.clients.get(deleted)
      if (ws !== undefined) {
        try {
          this.ctx.info('workspace removed from tracking list', { workspace: deleted })
          this.clients.delete(deleted)
          await ws.close()
        } catch (err: any) {
          Analytics.handleError(err)
          errors++
        }
      }
    }
    this.ctx.info('************************* Check workspaces done ************************* ', {
      workspaces: this.clients.size
    })
    return errors > 0
  }

  getWorkers (): GithubWorker[] {
    return Array.from(this.clients.values())
  }

  static async create (ctx: MeasureContext, app: App, brandingMap: BrandingMap): Promise<PlatformWorker> {
    const worker = new PlatformWorker(ctx, app, brandingMap)
    await worker.initStorage()
    await worker.init(ctx)
    worker.initWebhooks()
    return worker
  }

  initWebhooks (): void {
    const webhook = this.ctx.newChild(
      'webhook',
      {},
      {},
      new SplitLogger('webhook', { root: join(process.cwd(), 'logs'), pretty: true, enableConsole: false })
    )
    webhook.info('Register webhook')

    this.app.webhooks.onAny(async (event) => {
      const shortData: Record<string, string> = { id: event.id, name: event.name }
      if ('action' in event.payload) {
        shortData.action = event.payload.action
      }
      this.ctx.info('webhook event', shortData)
      webhook.info('event', { ...shortData, payload: event.payload })
    })

    this.app.webhooks.on('github_app_authorization', async (event) => {
      if (event.payload.action === 'revoked') {
        const sender = event.payload.sender

        const record = await this.getAccount(sender.login)
        if (record !== undefined) {
          await this.revokeUserAuth(record)
          await this.userManager.removeUser(sender.login)
        }
      }
    })

    this.app.webhooks.onError(async (event) => {
      this.ctx.error('webhook event', { message: event.message, name: event.name, cause: event.cause })
      webhook.error('event', { ...event })
    })

    function catchEventError (
      promise: Promise<void>,
      action: string,
      name: string,
      id: string,
      repository: string
    ): void {
      void promise.catch((err) => {
        webhook.error('error during handleEvent', { err, event: action, repository, name, id })
        Analytics.handleError(err)
      })
    }

    this.app.webhooks.on('pull_request', async ({ payload, name, id }) => {
      const repoWorker = this.getWorker(payload.installation?.id)
      if (repoWorker !== undefined) {
        catchEventError(
          repoWorker.handleEvent(github.class.GithubPullRequest, payload.installation?.id, payload),
          payload.action,
          name,
          id,
          payload.repository.name
        )
      }
    })

    this.app.webhooks.on('issues', async ({ payload, name, id }) => {
      const repoWorker = this.getWorker(payload.installation?.id)
      if (repoWorker !== undefined) {
        catchEventError(
          repoWorker.handleEvent(tracker.class.Issue, payload.installation?.id, payload),
          payload.action,
          name,
          id,
          payload.repository.name
        )
      }
    })
    this.app.webhooks.on('issue_comment', async ({ payload, name, id }) => {
      const repoWorker = this.getWorker(payload.installation?.id)
      if (repoWorker !== undefined) {
        catchEventError(
          repoWorker.handleEvent(chunter.class.ChatMessage, payload.installation?.id, payload),
          payload.action,
          name,
          id,
          payload.repository.name
        )
      }
    })

    this.app.webhooks.on('repository', async ({ payload, name, id }) => {
      const repoWorker = this.getWorker(payload.installation?.id)
      if (repoWorker !== undefined) {
        catchEventError(
          repoWorker.handleEvent(github.mixin.GithubProject, payload.installation?.id, payload),
          payload.action,
          name,
          id,
          payload.repository.name
        )
      }
    })

    this.app.webhooks.on('projects_v2_item', async ({ payload, name, id }) => {
      const repoWorker = this.getWorker(payload.installation?.id)
      if (repoWorker !== undefined) {
        if (payload.projects_v2_item.content_type === 'Issue') {
          catchEventError(
            repoWorker.handleEvent(tracker.class.Issue, payload.installation?.id, payload),
            payload.action,
            name,
            id,
            payload.projects_v2_item.node_id
          )
        } else if (payload.projects_v2_item.content_type === 'PullRequest') {
          catchEventError(
            repoWorker.handleEvent(github.class.GithubPullRequest, payload.installation?.id, payload),
            payload.action,
            name,
            id,
            payload.projects_v2_item.node_id
          )
        }
      }
    })

    this.app.webhooks.on('installation', async ({ payload, name, id }) => {
      switch (payload.action) {
        case 'created':
        case 'unsuspend': {
          catchEventError(
            this.handleInstallationEvent(payload.installation, payload.repositories, true),
            payload.action,
            name,
            id,
            payload.installation.html_url
          )
          break
        }
        case 'suspend': {
          catchEventError(
            this.handleInstallationEvent(payload.installation, payload.repositories, false),
            payload.action,
            name,
            id,
            payload.installation.html_url
          )
          break
        }
        case 'deleted': {
          catchEventError(
            this.handleInstallationEventDelete(payload.installation.id),
            payload.action,
            name,
            id,
            payload.installation.html_url
          )
          break
        }
      }
    })
    this.app.webhooks.on('installation_repositories', async ({ payload, name, id }) => {
      const worker = this.getWorker(payload.installation.id)
      if (worker === undefined) {
        this.triggerCheckWorkspaces()
        return
      }
      catchEventError(
        worker.reloadRepositories(payload.installation.id),
        payload.action,
        name,
        id,
        payload.installation.html_url
      )
      const doSyncUsers = async (worker: GithubWorker): Promise<void> => {
        const users = await this.getUsers(worker.workspace.name)
        await worker.syncUserData(this.ctx, users)
      }
      catchEventError(doSyncUsers(worker), payload.action, name, id, payload.installation.html_url)
    })

    this.app.webhooks.on('pull_request_review', async ({ payload, name, id }) => {
      const repoWorker = this.getWorker(payload.installation?.id)
      if (repoWorker !== undefined) {
        catchEventError(
          repoWorker.handleEvent(github.class.GithubReview, payload.installation?.id, payload),
          payload.action,
          name,
          id,
          payload.repository.html_url
        )
      }
    })

    this.app.webhooks.on('pull_request_review_comment', async ({ payload, name, id }) => {
      const repoWorker = this.getWorker(payload.installation?.id)
      if (repoWorker !== undefined) {
        catchEventError(
          repoWorker.handleEvent(github.class.GithubReviewComment, payload.installation?.id, payload),
          payload.action,
          name,
          id,
          payload.repository.html_url
        )
      }
    })
    this.app.webhooks.on('pull_request_review_thread', async ({ payload, name, id }) => {
      const repoWorker = this.getWorker(payload.installation?.id)
      if (repoWorker !== undefined) {
        catchEventError(
          repoWorker.handleEvent(github.class.GithubReviewThread, payload.installation?.id, payload),
          payload.action,
          name,
          id,
          payload.repository.html_url
        )
      }
    })
  }

  public async revokeUserAuth (record: GithubUserRecord): Promise<void> {
    for (const [ws, acc] of Object.entries(record.accounts)) {
      await this.updateAccountAuthRecord({ workspace: ws, accountId: acc }, { login: record._id }, undefined, true)
    }
  }

  getWorker (installationId?: number): GithubWorker | undefined {
    if (installationId === undefined) {
      return
    }
    for (const w of this.clients.values()) {
      for (const i of w.integrations.values()) {
        if (i.installationId === installationId) {
          return w
        }
      }
      for (const i of w.integrationsRaw.values()) {
        if (i.installationId === installationId) {
          return w
        }
      }
    }
  }
}