import ExitCodes from '../ExitCodes'
import { WorkingGroups } from '../Types'
import { CuratorGroup, CuratorGroupId, ContentActor, Channel } from '@joystream/types/content'
import { Worker } from '@joystream/types/working-group'
import { CLIError } from '@oclif/errors'
import { flags } from '@oclif/command'
import { memberHandle } from '../helpers/display'
import { MemberId } from '@joystream/types/common'
import { createType } from '@joystream/types'
import WorkingGroupCommandBase from './WorkingGroupCommandBase'

const CHANNEL_CREATION_CONTEXTS = ['Member', 'Curator'] as const
const CATEGORIES_CONTEXTS = ['Lead', 'Curator'] as const
const CHANNEL_MANAGEMENT_CONTEXTS = ['Owner', 'Collaborator'] as const

type ChannelManagementContext = typeof CHANNEL_MANAGEMENT_CONTEXTS[number]
type ChannelCreationContext = typeof CHANNEL_CREATION_CONTEXTS[number]
type CategoriesContext = typeof CATEGORIES_CONTEXTS[number]

/**
 * Abstract base class for commands related to content directory
 */
export default abstract class ContentDirectoryCommandBase extends WorkingGroupCommandBase {
  static flags = {
    ...WorkingGroupCommandBase.flags,
  }

  static channelCreationContextFlag = flags.enum({
    required: false,
    description: `Actor context to execute the command in (${CHANNEL_CREATION_CONTEXTS.join('/')})`,
    options: [...CHANNEL_CREATION_CONTEXTS],
  })

  static channelManagementContextFlag = flags.enum({
    required: false,
    description: `Actor context to execute the command in (${CHANNEL_MANAGEMENT_CONTEXTS.join('/')})`,
    options: [...CHANNEL_MANAGEMENT_CONTEXTS],
  })

  static categoriesContextFlag = flags.enum({
    required: false,
    description: `Actor context to execute the command in (${CATEGORIES_CONTEXTS.join('/')})`,
    options: [...CATEGORIES_CONTEXTS],
  })

  async init(): Promise<void> {
    await super.init()
    this._group = WorkingGroups.Curators // override group for RolesCommandBase
  }

  async promptForChannelCreationContext(
    message = 'Choose in which context you wish to execute the command'
  ): Promise<ChannelCreationContext> {
    return this.simplePrompt({
      message,
      type: 'list',
      choices: CHANNEL_CREATION_CONTEXTS.map((c) => ({ name: c, value: c })),
    })
  }

  async promptForCategoriesContext(
    message = 'Choose in which context you wish to execute the command'
  ): Promise<CategoriesContext> {
    return this.simplePrompt({
      message,
      type: 'list',
      choices: CATEGORIES_CONTEXTS.map((c) => ({ name: c, value: c })),
    })
  }

  // Use when lead access is required in given command
  async requireLead(): Promise<void> {
    await this.getRequiredLeadContext()
  }

  getCurationActorByChannel(channel: Channel): Promise<[ContentActor, string]> {
    return channel.owner.isOfType('Curators') ? this.getContentActor('Lead') : this.getContentActor('Curator')
  }

  async getChannelOwnerActor(channel: Channel): Promise<[ContentActor, string]> {
    if (channel.owner.isOfType('Curators')) {
      try {
        return this.getContentActor('Lead')
      } catch (e) {
        return this.getCuratorContext(channel.owner.asType('Curators'))
      }
    } else {
      const { id, membership } = await this.getRequiredMemberContext(false, [channel.owner.asType('Member')])
      return [
        createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
        membership.controller_account.toString(),
      ]
    }
  }

  async getChannelCollaboratorActor(channel: Channel): Promise<[ContentActor, string]> {
    const { id, membership } = await this.getRequiredMemberContext(false, Array.from(channel.collaborators))
    return [
      createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
      membership.controller_account.toString(),
    ]
  }

  isChannelOwner(channel: Channel, actor: ContentActor): boolean {
    return channel.owner.isOfType('Curators')
      ? (actor.isOfType('Curator') && actor.asType('Curator')[0].eq(channel.owner.asType('Curators'))) ||
          actor.isOfType('Lead')
      : actor.isOfType('Member') && actor.asType('Member').eq(channel.owner.asType('Member'))
  }

  async getChannelManagementActor(
    channel: Channel,
    context: ChannelManagementContext
  ): Promise<[ContentActor, string]> {
    if (context && context === 'Owner') {
      return this.getChannelOwnerActor(channel)
    }
    if (context && context === 'Collaborator') {
      return this.getChannelCollaboratorActor(channel)
    }

    // Context not set - derive

    try {
      const owner = await this.getChannelOwnerActor(channel)
      this.log('Derived context: Channel owner')
      return owner
    } catch (e) {
      // continue
    }

    try {
      const collaborator = await this.getChannelCollaboratorActor(channel)
      this.log('Derived context: Channel collaborator')
      return collaborator
    } catch (e) {
      // continue
    }

    this.error('No account found with access to manage the provided channel', { exit: ExitCodes.AccessDenied })
  }

  async getCategoryManagementActor(): Promise<[ContentActor, string]> {
    try {
      const lead = await this.getContentActor('Lead')
      this.log('Derived context: Lead')
      return lead
    } catch (e) {
      // continue
    }
    try {
      const curator = await this.getContentActor('Curator')
      this.log('Derived context: Curator')
      return curator
    } catch (e) {
      // continue
    }

    this.error('Lead / Curator Group member permissions are required for this action', { exit: ExitCodes.AccessDenied })
  }

  async getCuratorContext(requiredGroupId?: CuratorGroupId): Promise<[ContentActor, string]> {
    const curator = await this.getRequiredWorkerContext()

    let groupId: number
    if (requiredGroupId) {
      const group = await this.getCuratorGroup(requiredGroupId.toNumber())
      if (!group.active.valueOf()) {
        this.error(`Curator group ${requiredGroupId.toString()} is no longer active`, { exit: ExitCodes.AccessDenied })
      }
      if (!Array.from(group.curators).some((curatorId) => curatorId.eq(curator.workerId))) {
        this.error(`You don't belong to required curator group (ID: ${requiredGroupId.toString()})`, {
          exit: ExitCodes.AccessDenied,
        })
      }
      groupId = requiredGroupId.toNumber()
    } else {
      const groups = await this.getApi().availableCuratorGroups()
      const availableGroupIds = groups
        .filter(
          ([, group]) =>
            group.active.valueOf() && Array.from(group.curators).some((curatorId) => curatorId.eq(curator.workerId))
        )
        .map(([id]) => id)

      if (!availableGroupIds.length) {
        this.error("You don't belong to any active curator group!", { exit: ExitCodes.AccessDenied })
      } else if (availableGroupIds.length === 1) {
        groupId = availableGroupIds[0].toNumber()
      } else {
        groupId = await this.promptForCuratorGroup('Select Curator Group context', availableGroupIds)
      }
    }

    return [
      createType<ContentActor, 'ContentActor'>('ContentActor', { Curator: [groupId, curator.workerId.toNumber()] }),
      curator.roleAccount.toString(),
    ]
  }

  private async curatorGroupChoices(ids?: CuratorGroupId[]) {
    const groups = await this.getApi().availableCuratorGroups()
    return groups
      .filter(([id]) => (ids ? ids.some((allowedId) => allowedId.eq(id)) : true))
      .map(([id, group]) => ({
        name:
          `Group ${id.toString()} (` +
          `${group.active.valueOf() ? 'Active' : 'Inactive'}, ` +
          `${Array.from(group.curators).length} member(s)), `,
        value: id.toNumber(),
      }))
  }

  async promptForCuratorGroup(message = 'Select a Curator Group', ids?: CuratorGroupId[]): Promise<number> {
    const choices = await this.curatorGroupChoices(ids)
    if (!choices.length) {
      this.warn('No Curator Groups to choose from!')
      this.exit(ExitCodes.InvalidInput)
    }
    const selectedId = await this.simplePrompt<number>({ message, type: 'list', choices })

    return selectedId
  }

  async promptForCuratorGroups(message = 'Select Curator Groups'): Promise<number[]> {
    const choices = await this.curatorGroupChoices()
    if (!choices.length) {
      return []
    }
    const selectedIds = await this.simplePrompt<number[]>({ message, type: 'checkbox', choices })

    return selectedIds
  }

  async promptForCurator(message = 'Choose a Curator', ids?: number[]): Promise<number> {
    const curators = await this.getApi().groupMembers(WorkingGroups.Curators)
    const choices = curators
      .filter((c) => (ids ? ids.includes(c.workerId.toNumber()) : true))
      .map((c) => ({
        name: `${memberHandle(c.profile)} (Worker ID: ${c.workerId})`,
        value: c.workerId.toNumber(),
      }))

    if (!choices.length) {
      this.warn('No Curators to choose from!')
      this.exit(ExitCodes.InvalidInput)
    }

    const selectedCuratorId = await this.simplePrompt<number>({
      message,
      type: 'list',
      choices,
    })

    return selectedCuratorId
  }

  async getCurator(id: string | number): Promise<Worker> {
    if (typeof id === 'string') {
      id = parseInt(id)
    }

    let curator
    try {
      curator = await this.getApi().workerByWorkerId(WorkingGroups.Curators, id)
    } catch (e) {
      if (e instanceof CLIError) {
        throw new CLIError('Invalid Curator id!')
      }
      throw e
    }

    return curator
  }

  async getCuratorGroup(id: string | number): Promise<CuratorGroup> {
    if (typeof id === 'string') {
      id = parseInt(id)
    }

    const group = await this.getApi().curatorGroupById(id)

    if (!group) {
      this.error('Invalid Curator Group id!', { exit: ExitCodes.InvalidInput })
    }

    return group
  }

  async getContentActor(
    context: Exclude<keyof typeof ContentActor.typeDefinitions, 'Collaborator'>
  ): Promise<[ContentActor, string]> {
    if (context === 'Member') {
      const { id, membership } = await this.getRequiredMemberContext()
      return [
        createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
        membership.controller_account.toString(),
      ]
    }

    if (context === 'Curator') {
      return this.getCuratorContext()
    }

    if (context === 'Lead') {
      const lead = await this.getRequiredLeadContext()
      return [createType<ContentActor, 'ContentActor'>('ContentActor', { Lead: null }), lead.roleAccount.toString()]
    }

    throw new Error(`Unrecognized context: ${context}`)
  }

  async validateMemberIdsSet(ids: number[] | MemberId[], setName: 'collaborator' | 'moderator'): Promise<void> {
    const members = await this.getApi().getMembers(ids)
    if (members.length < ids.length || members.some((m) => m.isEmpty)) {
      this.error(`Invalid ${setName} set! All ${setName} set members must be existing members!`, {
        exit: ExitCodes.InvalidInput,
      })
    }
  }
}