import core, {
  WorkspaceEvent,
  generateId,
  getTypeOf,
  type BulkUpdateEvent,
  type Class,
  type Doc,
  type FullParamsType,
  type MeasureContext,
  type ParamsType,
  type Ref,
  type TxWorkspaceEvent,
  type WorkspaceIdWithUrl,
  type Branding,
  type BrandingMap
} from '@hcengineering/core'
import { type Hash } from 'crypto'
import fs from 'fs'
import type { SessionContext } from './types'

/**
 * Return some estimation for object size
 */
export function estimateDocSize (_obj: any): number {
  let result = 0
  const toProcess = [_obj]
  while (toProcess.length > 0) {
    const obj = toProcess.shift()
    if (typeof obj === 'undefined') {
      continue
    }
    if (typeof obj === 'function') {
      continue
    }
    for (const key in obj) {
      // include prototype properties
      const value = obj[key]
      const type = getTypeOf(value)
      result += key.length

      switch (type) {
        case 'Array':
          result += 4
          toProcess.push(value)
          break
        case 'Object':
          toProcess.push(value)
          break
        case 'Date':
          result += 24 // Some value
          break
        case 'string':
          result += (value as string).length
          break
        case 'number':
          result += 8
          break
        case 'boolean':
          result += 1
          break
        case 'symbol':
          result += (value as symbol).toString().length
          break
        case 'bigint':
          result += (value as bigint).toString().length
          break
        case 'undefined':
          result += 1
          break
        case 'null':
          result += 1
          break
        default:
          result += value.toString().length
      }
    }
  }
  return result
}
/**
 * Return some estimation for object size
 */
export function updateHashForDoc (hash: Hash, _obj: any): void {
  const toProcess = [_obj]
  while (toProcess.length > 0) {
    const obj = toProcess.shift()
    if (typeof obj === 'undefined') {
      continue
    }
    if (typeof obj === 'function') {
      continue
    }
    for (const key in obj) {
      // include prototype properties
      const value = obj[key]
      const type = getTypeOf(value)
      hash.update(key)

      switch (type) {
        case 'Array':
          toProcess.push(value)
          break
        case 'Object':
          toProcess.push(value)
          break
        case 'Date':
          hash.update(value.toString())
          break
        case 'string':
          hash.update(value)
          break
        case 'number':
          hash.update((value as number).toString(16))
          break
        case 'boolean':
          hash.update((value as boolean) ? '1' : '0')
          break
        case 'symbol':
          hash.update((value as symbol).toString())
          break
        case 'bigint':
          hash.update((value as bigint).toString())
          break
        case 'undefined':
          hash.update('und')
          break
        case 'null':
          hash.update('null')
          break
        default:
          hash.update(value.toString())
      }
    }
  }
}

export class SessionContextImpl implements SessionContext {
  constructor (
    readonly ctx: MeasureContext,
    readonly userEmail: string,
    readonly sessionId: string,
    readonly admin: boolean | undefined,
    readonly derived: SessionContext['derived'],
    readonly workspace: WorkspaceIdWithUrl,
    readonly branding: Branding | null,
    readonly isAsyncContext: boolean
  ) {}

  with<T>(
    name: string,
    params: ParamsType,
    op: (ctx: SessionContext) => T | Promise<T>,
    fullParams?: FullParamsType
  ): Promise<T> {
    return this.ctx.with(
      name,
      params,
      async (ctx) =>
        await op(
          new SessionContextImpl(
            ctx,
            this.userEmail,
            this.sessionId,
            this.admin,
            this.derived,
            this.workspace,
            this.branding,
            this.isAsyncContext
          )
        ),
      fullParams
    )
  }
}

export function createBroadcastEvent (classes: Ref<Class<Doc>>[]): TxWorkspaceEvent<BulkUpdateEvent> {
  return {
    _class: core.class.TxWorkspaceEvent,
    _id: generateId(),
    event: WorkspaceEvent.BulkUpdate,
    params: {
      _class: classes
    },
    modifiedBy: core.account.System,
    modifiedOn: Date.now(),
    objectSpace: core.space.DerivedTx,
    space: core.space.DerivedTx
  }
}

export function loadBrandingMap (brandingPath?: string): BrandingMap {
  let brandings: BrandingMap = {}
  if (brandingPath !== undefined && brandingPath !== '') {
    brandings = JSON.parse(fs.readFileSync(brandingPath, 'utf8'))

    for (const [host, value] of Object.entries(brandings)) {
      const protocol = value.protocol ?? 'https'
      value.front = `${protocol}://${host}/`
    }
  }

  return brandings
}