import type {
  OrganizationModel,
  OrganizationModelSurface,
  OrganizationModelSidebarNode,
  OrganizationModelSidebarSurfaceNode,
  OrganizationModelSystemEntry
} from './types'
import { listAllSystems } from './helpers'
import { getSortedSidebarEntries } from './domains/navigation'

export type OrganizationSurfaceProjectionIssueCode =
  | 'duplicate-surface-id'
  | 'duplicate-surface-path'
  | 'unknown-surface-system'

export interface OrganizationSurfaceProjection {
  id: string
  label: string
  path: string
  surfaceType: OrganizationModelSurface['surfaceType']
  description?: string
  icon?: string
  order?: number
  systemIds: string[]
  entityIds: string[]
  resourceIds: string[]
  actionIds: string[]
  enabled: boolean
  devOnly?: boolean
  requiresAdmin?: boolean
}

export interface OrganizationSurfaceProjectionIssue {
  code: OrganizationSurfaceProjectionIssueCode
  message: string
  path: Array<string | number>
  surfaceId?: string
  systemId?: string
  value?: string
}

interface SidebarSurfaceWithId {
  id: string
  surface: OrganizationModelSidebarSurfaceNode
  path: Array<string | number>
}

function normalizePath(path: string): string {
  return path.length > 1 ? path.replace(/\/+$/, '') : path
}

function collectSystemsById(model: OrganizationModel): Map<string, OrganizationModelSystemEntry> {
  const systemsById = new Map<string, OrganizationModelSystemEntry>()
  for (const { path, system } of listAllSystems(model)) {
    systemsById.set(path, system)
    systemsById.set(system.id, system)
  }
  return systemsById
}

function getSystemWithAncestors(
  systemsById: Map<string, OrganizationModelSystemEntry>,
  systemId: string
): OrganizationModelSystemEntry[] {
  const systems: OrganizationModelSystemEntry[] = []
  let current = systemsById.get(systemId)
  while (current !== undefined) {
    systems.unshift(current)
    current = current.parentSystemId ? systemsById.get(current.parentSystemId) : undefined
  }
  return systems
}

function hasInheritedFlag(
  systemsById: Map<string, OrganizationModelSystemEntry>,
  systemIds: string[],
  flag: 'devOnly' | 'requiresAdmin'
): boolean {
  return systemIds.some((systemId) =>
    getSystemWithAncestors(systemsById, systemId).some((system) => system[flag] === true)
  )
}

function isLifecycleEnabled(system: OrganizationModelSystemEntry): boolean {
  if (system.enabled === false) return false
  return system.lifecycle !== 'deprecated' && system.lifecycle !== 'archived'
}

function isLifecycleDevOnly(system: OrganizationModelSystemEntry): boolean {
  return system.devOnly === true || system.lifecycle === 'beta'
}

function isSystemEnabled(systemsById: Map<string, OrganizationModelSystemEntry>, systemId: string): boolean {
  const systemLineage = getSystemWithAncestors(systemsById, systemId)
  return systemLineage.length > 0 && systemLineage.every(isLifecycleEnabled)
}

function unique(values: string[]): string[] {
  return [...new Set(values)]
}

function collectSidebarSurfaces(
  nodes: Record<string, OrganizationModelSidebarNode>,
  schemaPath: Array<string | number>,
  surfaces: SidebarSurfaceWithId[] = []
): SidebarSurfaceWithId[] {
  getSortedSidebarEntries(nodes).forEach(([id, node]) => {
    const nodePath = [...schemaPath, id]
    if (node.type === 'group') {
      collectSidebarSurfaces(node.children, [...nodePath, 'children'], surfaces)
      return
    }
    surfaces.push({ id, surface: node, path: nodePath })
  })

  return surfaces
}

function getSidebarSurfaces(model: OrganizationModel): SidebarSurfaceWithId[] {
  return [
    ...collectSidebarSurfaces(model.navigation.sidebar.primary, ['navigation', 'sidebar', 'primary']),
    ...collectSidebarSurfaces(model.navigation.sidebar.bottom, ['navigation', 'sidebar', 'bottom'])
  ]
}

export function projectOrganizationSurfaces(model: OrganizationModel): OrganizationSurfaceProjection[] {
  const systemsById = collectSystemsById(model)

  return getSidebarSurfaces(model).map(({ id, surface }) => {
    const targets = surface.targets ?? {}
    const systemIds = unique((targets.systems ?? []).filter((systemId: string) => systemsById.has(systemId)))
    const enabled = systemIds.every((candidate) => isSystemEnabled(systemsById, candidate))
    const devOnly =
      surface.devOnly === true ||
      hasInheritedFlag(systemsById, systemIds, 'devOnly') ||
      systemIds.some((candidate) => getSystemWithAncestors(systemsById, candidate).some(isLifecycleDevOnly))
    const requiresAdmin = hasInheritedFlag(systemsById, systemIds, 'requiresAdmin')

    return {
      id,
      label: surface.label,
      path: surface.path,
      surfaceType: surface.surfaceType,
      ...(surface.description !== undefined ? { description: surface.description } : {}),
      ...(surface.icon !== undefined ? { icon: surface.icon } : {}),
      ...(surface.order !== undefined ? { order: surface.order } : {}),
      systemIds,
      entityIds: [...(targets.entities ?? [])],
      resourceIds: [...(targets.resources ?? [])],
      actionIds: [...(targets.actions ?? [])],
      enabled,
      ...(devOnly ? { devOnly } : {}),
      ...(surface.requiresAdmin === true || requiresAdmin ? { requiresAdmin: true } : {})
    }
  })
}

export function validateOrganizationSurfaceProjection(model: OrganizationModel): OrganizationSurfaceProjectionIssue[] {
  const issues: OrganizationSurfaceProjectionIssue[] = []
  const systemsById = collectSystemsById(model)
  const surfaceIds = new Set<string>()
  const surfacePaths = new Map<string, string>()

  function addIssue(issue: OrganizationSurfaceProjectionIssue): void {
    issues.push(issue)
  }

  getSidebarSurfaces(model).forEach(({ id, surface, path }) => {
    if (surfaceIds.has(id)) {
      addIssue({
        code: 'duplicate-surface-id',
        message: `Surface id "${id}" must be unique`,
        path,
        surfaceId: id,
        value: id
      })
    } else {
      surfaceIds.add(id)
    }

    const normalizedPath = normalizePath(surface.path)
    const existingSurfaceId = surfacePaths.get(normalizedPath)
    if (existingSurfaceId !== undefined) {
      addIssue({
        code: 'duplicate-surface-path',
        message: `Surface path "${surface.path}" is already used by surface "${existingSurfaceId}"`,
        path: [...path, 'path'],
        surfaceId: id,
        value: surface.path
      })
    } else {
      surfacePaths.set(normalizedPath, id)
    }

    surface.targets?.systems?.forEach((systemId: string, systemIndex: number) => {
      if (systemsById.has(systemId)) {
        return
      }

      addIssue({
        code: 'unknown-surface-system',
        message: `Surface "${id}" references unknown system "${systemId}"`,
        path: [...path, 'targets', 'systems', systemIndex],
        surfaceId: id,
        systemId,
        value: systemId
      })
    })
  })

  return issues
}
