import { groupBy, isEqual } from 'lodash'

import {
  OrganizationRole,
  OrganizationRolesResponse,
} from '@/data/organization-members/organization-roles-query'
import { OrganizationMember } from '@/data/organizations/organization-members-query'

export interface ProjectRoleConfiguration {
  ref?: string
  name?: string
  roleId: number
  baseRoleId?: number
}

export const formatMemberRoleToProjectRoleConfiguration = (
  member: OrganizationMember,
  allRoles: OrganizationRolesResponse
) => {
  const { org_scoped_roles, project_scoped_roles } = allRoles

  const roleConfiguration = member.role_ids
    .map((id) => {
      const orgRole = org_scoped_roles.find((role) => role.id === id)
      if (orgRole !== undefined) {
        return { ref: undefined, name: undefined, roleId: orgRole.id }
      }

      const projectRole = project_scoped_roles.find((role) => role.id === id)
      if (projectRole !== undefined) {
        return projectRole.projects.map((x) => ({
          ref: x.ref,
          name: x.name,
          roleId: projectRole.id,
          baseRoleId: projectRole.base_role_id,
        }))
      }

      return undefined
    })
    .filter(Boolean)
    .flat()
    .filter((p) => {
      // [Joshen] Validate only for project scoped roles
      // This filters out project scoped roles for projects that the user doesn't have access to
      // (e.g if the project was deleted)
      if ('baseRoleId' in p!) {
        return p.ref !== undefined
      } else {
        return p
      }
    })
    .sort((a, b) => (a?.name ?? '').localeCompare(b?.name ?? '')) as ProjectRoleConfiguration[]

  return roleConfiguration
}

export const deriveChanges = (
  original: ProjectRoleConfiguration[] = [],
  final: ProjectRoleConfiguration[] = []
) => {
  const removed: ProjectRoleConfiguration[] = []
  const added: ProjectRoleConfiguration[] = []
  const updated: {
    ref?: string
    name?: string
    originalRole: number
    originalBaseRole?: number
    updatedRole: number
  }[] = []

  original.forEach((x) => {
    const updatedRoleForProject = final.find((y) => x.ref === y.ref)
    if (updatedRoleForProject === undefined) {
      removed.push(x)
    } else if (updatedRoleForProject.roleId !== x.roleId) {
      updated.push({
        ref: updatedRoleForProject.ref,
        name: updatedRoleForProject.name,
        originalRole: x.roleId,
        originalBaseRole: x.baseRoleId,
        updatedRole: updatedRoleForProject.roleId,
      })
    }
  })

  final.forEach((x) => {
    const newRoleForProject = original.find((y) => x.ref === y.ref)
    if (newRoleForProject === undefined) {
      added.push(x)
    }
  })

  return { removed, added, updated }
}

// [Joshen] Essentially spit out 3 arrays
// What to (1) Remove (2) Assign (3) Update
// And so that we can unit test this
export const deriveRoleChangeActions = (
  existingRoles: OrganizationRole[],
  changesToRoles: {
    removed: ProjectRoleConfiguration[]
    added: ProjectRoleConfiguration[]
    updated: {
      ref?: string
      name?: string
      originalRole: number
      originalBaseRole?: number
      updatedRole: number
    }[]
  }
) => {
  const { removed, added, updated } = changesToRoles
  const toRemove: number[] = []
  const toAssign: { roleId: number; refs: string[] }[] = []
  const toUpdate: { roleId: number; refs: string[] }[] = []

  const groupByAddedRoles = groupBy(added, 'roleId')
  const groupByRemovedRoles = groupBy(removed, 'roleId')
  const groupByUpdatingFromRoles = groupBy(updated, 'originalRole')
  const groupByUpdatingToRoles = groupBy(updated, 'updatedRole')
  const existingProjectRolesByBaseIds = existingRoles
    .filter((r) => (r?.projects ?? []).length > 0)
    .map((r) => r.base_role_id)

  existingRoles.forEach((role) => {
    const projectRefsApplied = role.projects.map((x) => x.ref)
    if (projectRefsApplied.length === 0) {
      // [Joshen] In this case we're removing an org scope role, skip all the chekcs
      return toRemove.push(role.id)
    }

    const toRemoveRole = projectRefsApplied.every((ref) => {
      const isRemoved = removed.map((r) => r.ref).some((x) => x === ref)
      const isUpdated = updated.map((r) => r.ref).some((x) => x === ref)
      return isRemoved || isUpdated
    })
    const isRoleGettingAdded = added.some((r) => r.roleId === role.base_role_id)
    const isRoleGettingUpdatedTo = updated.some((r) => r.updatedRole === role.base_role_id)

    if (toRemoveRole && !isRoleGettingAdded && !isRoleGettingUpdatedTo) {
      return toRemove.push(role.id)
    }

    const projectsToAddToRole = (groupByAddedRoles[role.base_role_id]?.map((r) => r.ref) ??
      []) as string[]
    const projectsToRemoveFromRole = (groupByRemovedRoles[role.id]?.map((r) => r.ref) ??
      []) as string[]
    const projectsUpdatingFromRole = (groupByUpdatingFromRoles[role.id]?.map((r) => r.ref) ??
      []) as string[]
    const projectsUpdatingToRole = (groupByUpdatingToRoles[role.base_role_id]?.map((r) => r.ref) ??
      []) as string[]
    const projectRefsAppliedUpdated = projectRefsApplied
      .filter((x) => !projectsToRemoveFromRole.includes(x))
      .filter((x) => !projectsUpdatingFromRole.includes(x))
      .concat(projectsToAddToRole)
      .concat(projectsUpdatingToRole)

    if (!isEqual(projectRefsApplied, projectRefsAppliedUpdated)) {
      toUpdate.push({ roleId: role.id, refs: projectRefsAppliedUpdated })
    }
  })

  Object.keys(groupByAddedRoles).forEach((roleId) => {
    if (!existingProjectRolesByBaseIds.includes(Number(roleId))) {
      toAssign.push({
        roleId: Number(roleId),
        refs: groupByAddedRoles[roleId].map((x) => x.ref).filter((x) => x !== undefined),
      })
    }
  })

  Object.keys(groupByUpdatingToRoles).forEach((roleId) => {
    if (!existingProjectRolesByBaseIds.includes(Number(roleId))) {
      toAssign.push({
        roleId: Number(roleId),
        refs: groupByUpdatingToRoles[roleId].map((x) => x.ref).filter((x) => x !== undefined),
      })
    }
  })

  // [Joshen] Am sorting the results just so its more deterministic when writing tests
  return {
    toRemove: toRemove.sort((a, b) => a - b),
    toAssign: toAssign.map((x) => ({ ...x, refs: x.refs.sort((a, b) => a.localeCompare(b)) })),
    toUpdate: toUpdate.map((x) => ({ ...x, refs: x.refs.sort((a, b) => a.localeCompare(b)) })),
  }
}
