import { LitElement, TemplateResult, html, nothing } from 'lit'
import { property, state } from 'lit/decorators.js'
import type {
  AccessOperation,
  IAccessAddOperation,
  IAccessRemoveOperation,
  IAccessUpdateOperation,
  IPermission,
} from '@api-client/core/models/store/Permission.js'
import type { IUser } from '@api-client/core/models/store/User.js'
import type { PermissionRole } from '@api-client/core/models/store/Permission.js'
import { ClassInfo, classMap } from 'lit/directives/class-map.js'

/**
 * An element that manages the internal state and logic for sharing a file.
 *
 * This component handles the different states of the file sharing process, including initialization,
 * readiness, error handling, and committing changes. However, the component does not implement any API calls.
 * The hosting application is responsible for performing the actual file sharing operations.
 *
 * The component is not 100% complete. Pending features include:
 * - Permissions on groups.
 * - Sharing with the organization.
 * - Moving ownership to another user.
 *
 * @fires search-users - Dispatched when the user types in the search input.
 *                     The detail object contains the `query` string.
 * @fires grant-changed - Dispatched when the grant type for new shares is changed.
 *                      The detail object contains the `grant` role.
 * @fires pending-changed - Dispatched when a permission change for an existing user is staged.
 *                        The detail object contains the `pending` map of operations.
 */
export default class ShareFile extends LitElement {
  /**
   * The users available for sharing.
   * This property should be set by the hosting application in response to `search-users` events.
   */
  @property({ type: Array, attribute: false })
  accessor userSuggestions: IUser[] = []

  /**
   * The current grant/permission role for new shares.
   *
   * @attribute
   */
  @property({ type: String })
  accessor grant: PermissionRole = 'writer'

  /**
   * The users that already have permissions.
   */
  @property({ type: Array, attribute: false })
  accessor permissionUsers: IUser[] = []

  /**
   * The existing permissions on the file(s).
   *
   * - When there's only one file, this will contain the existing permissions for that file.
   * - When multiple files are being shared, this will contain the common permissions
   *   that can be set for all files.
   */
  @property({ type: Array, attribute: false })
  accessor permissions: IPermission[] = []

  /**
   * A flag indicating whether multiple files are being shared.
   * @attribute
   */
  @property({ type: Boolean })
  accessor multiple = false

  /**
   * Pending operations that haven't been committed yet
   */
  @state()
  accessor pending = new Map<string, AccessOperation>()

  /**
   * A map to store user details for pending add operations.
   */
  @state()
  protected accessor _addedUsers = new Map<string, IUser>()

  /**
   * An error message to display when the `currentState` is set to `Error`.
   * @attribute
   */
  @property() accessor error: string | undefined

  /**
   * A handler for the input event on the user search field.
   * Dispatches the `search-users` event.
   *
   * @param event The input event
   */
  protected handleNameInput(event: InputEvent): void {
    const input = event.target as HTMLInputElement
    const value = input.value.trim()
    if (!value) {
      return
    }
    this.dispatchEvent(
      new CustomEvent('search-users', {
        detail: { query: value },
        bubbles: false,
        composed: false,
      })
    )
  }

  /**
   * A handler for the keydown event on the user search field.
   * It focuses on the last selected user when Backspace is pressed and the input is empty.
   *
   * @param event The keyboard event
   */
  protected handleNameInputKeydown(event: KeyboardEvent): void {
    if (event.key === 'Backspace') {
      const input = event.target as HTMLInputElement
      if (input.value.length === 0) {
        // Check if we have any pending adds (newly selected users)
        // Since pending is a Map, we can check size, but we need to ensure they are adds.
        const hasAdds = Array.from(this.pending.values()).some((op) => op.op === 'add')
        if (hasAdds) {
          // If the input is empty and there are selected users, focus the last chip.
          const lastChip = this.renderRoot.querySelector<HTMLElement>('.share-input .chip:last-of-type')
          if (lastChip) {
            lastChip.focus()
          }
        }
      }
    }
  }

  /**
   * A handler for keydown events on the user suggestions list.
   * Stops propagation of the Escape key to prevent closing parent dialogs unexpectedly.
   *
   * @param event The keyboard event
   */
  protected handleUserSuggestionsKeydown(event: KeyboardEvent): void {
    if (event.key === 'Escape') {
      event.stopPropagation()
    }
  }

  /**
   * A handler for the autocomplete event when a user suggestion is selected.
   * Adds the user to the pending operations and dispatches `pending-changed`.
   *
   * @param event The custom event from the autocomplete input containing the selected item
   */
  protected handleUserSuggestion(event: CustomEvent<{ item: HTMLElement }>): void {
    const { key } = event.detail.item.dataset
    if (!key) {
      return
    }
    const index = this.userSuggestions.findIndex((u) => u.key === key)
    if (index === -1) {
      // User not found in suggestions, do nothing.
      return
    }
    const user = this.userSuggestions[index]
    if (this.pending.has(user.key)) {
      // User already in pending, do nothing.
      return
    }

    const op: IAccessAddOperation = {
      op: 'add',
      id: user.key,
      value: this.grant,
      type: 'user',
    }

    this.pending.set(user.key, op)
    this._addedUsers.set(user.key, user)

    this.dispatchEvent(
      new CustomEvent('pending-changed', {
        detail: { pending: this.pending },
        composed: true,
        bubbles: true,
      })
    )

    const input = this.renderRoot.querySelector<HTMLInputElement>('.native-input')
    if (input) {
      input.value = ''
    }
    // Remove from suggestions
    this.userSuggestions = this.userSuggestions.filter((u) => u.key !== key)
  }

  /**
   * A handler for the remove event on a user chip.
   * Removing a user chip puts the user back into the suggestions list.
   *
   * @param e The event triggered by the chip removal
   */
  protected handleChipClose(e: Event): void {
    const chip = e.target as HTMLElement
    const key = chip.dataset.key
    if (!key) {
      return
    }

    if (this.pending.has(key)) {
      this.pending.delete(key)
      const removedUser = this._addedUsers.get(key)
      this._addedUsers.delete(key)

      if (removedUser) {
        this.userSuggestions = [...this.userSuggestions, removedUser]
      }

      this.dispatchEvent(
        new CustomEvent('pending-changed', {
          detail: { pending: this.pending },
          composed: true,
          bubbles: true,
        })
      )
    }

    const input = this.renderRoot.querySelector<HTMLInputElement>('.native-input')
    if (input) {
      input.focus()
    }
  }

  /**
   * Focuses the input when the container is clicked to ensure a good UX.
   *
   * @param event The mouse event on the container
   */
  protected handleInputContainerClick(event: MouseEvent): void {
    const initialTarget = event.composedPath()[0] as HTMLElement
    if (initialTarget.classList.contains('input-container')) {
      const input = this.renderRoot.querySelector<HTMLInputElement>('.native-input')
      if (input) {
        input.focus()
      }
    }
  }

  /**
   * Handles the change event of the grant type selector for new shares.
   * Dispatches `grant-changed`.
   *
   * @param e The change event
   */
  protected handleGrantTypeChange(e: Event): void {
    const node = e.currentTarget as HTMLSelectElement
    const { value } = node
    if (!value) {
      return
    }
    this.grant = value as PermissionRole

    // Update pending adds with new role
    let hasChanges = false
    for (const [, op] of this.pending) {
      if (op.op === 'add') {
        if (op.value !== this.grant) {
          op.value = this.grant
          hasChanges = true
        }
      }
    }

    this.dispatchEvent(
      new CustomEvent('grant-changed', {
        detail: { grant: this.grant },
        composed: true,
        bubbles: true,
      })
    )

    if (hasChanges) {
      this.dispatchEvent(
        new CustomEvent('pending-changed', {
          detail: { pending: this.pending },
          composed: true,
          bubbles: true,
        })
      )
      this.requestUpdate()
    }
  }

  /**
   * Handles changes to the permission role of an existing user.
   * Updates the pending operations and dispatches `pending-changed`.
   *
   * @param e The change event
   */
  protected handleUserRoleChange(e: Event): void {
    const node = e.currentTarget as HTMLSelectElement
    const { value, name } = node
    if (!value || !name.startsWith('permission-')) {
      return
    }
    const key = name.replace('permission-', '')
    const permission = this.permissions.find((p) => p.granteeId === key)
    if (!permission) {
      return
    }
    if (value === 'remove') {
      const patch: IAccessRemoveOperation = {
        op: 'remove',
        id: permission.key,
        type: permission.type,
      }
      this.pending.set(permission.key, patch)
    } else {
      const newRole = value as PermissionRole
      if (newRole === permission.role) {
        this.pending.delete(permission.key)
      } else {
        const patch: IAccessUpdateOperation = {
          op: 'update',
          id: permission.key,
          type: permission.type,
          value: newRole,
        }
        this.pending.set(permission.key, patch)
      }
    }
    this.dispatchEvent(
      new CustomEvent('pending-changed', {
        detail: { pending: this.pending },
        composed: true,
        bubbles: true,
      })
    )
    this.requestUpdate()
  }

  protected override render(): TemplateResult {
    return html`${this.renderIntro()} ${this.renderInput()} ${this.renderPeopleWithAccess()}`
  }

  /**
   * Renders the introductory text based on single or multiple file sharing.
   */
  protected renderIntro(): TemplateResult {
    if (this.multiple) {
      return html`<p>Share the selected files with other users in your organization.</p>`
    }
    return html`<p>Share the file with other users in your organization.</p>`
  }

  /**
   * Renders the main input area including chips, text input and grant type selector.
   */
  protected renderInput(): TemplateResult {
    const addedUsers = Array.from(this.pending.values())
      .filter((op) => op.op === 'add')
      .map((op) => this._addedUsers.get(op.id)) // op.id is key/granteeId for add ops
      .filter((u): u is IUser => !!u)

    return html`<div class="share-input">
      <autocomplete-input
        class="autocomplete"
        @keydown="${this.handleUserSuggestionsKeydown}"
        @autocomplete="${this.handleUserSuggestion}"
      >
        <div slot="anchor" class="input-container" @click="${this.handleInputContainerClick}">
          ${addedUsers.map((user) => this.renderChip(user))}
          <input
            slot="input"
            class="native-input"
            type="text"
            name="input"
            autocomplete="off"
            placeholder="Enter name or email"
            @input="${this.handleNameInput}"
            @keydown="${this.handleNameInputKeydown}"
          />
        </div>
        ${this.renderUserSuggestions()}
      </autocomplete-input>
      ${this.renderGrantTypeSelector()}
    </div>`
  }

  /**
   * Renders the list of user suggestions for autocomplete.
   */
  protected renderUserSuggestions(): TemplateResult | typeof nothing {
    if (this.userSuggestions.length === 0) {
      return nothing
    }
    return html`<ui-listbox class="user-suggestions" slot="suggestions" aria-label="User suggestions">
      ${this.userSuggestions.map((user) => this.renderSuggestion(user))}
    </ui-listbox>`
  }

  /**
   * Renders a single user suggestion item.
   * @param user The user object to render
   */
  protected renderSuggestion(user: IUser): TemplateResult {
    const email = user.email[0]?.email || ''
    return html`<ui-list-item
      lines="two"
      image="avatar"
      data-key="${user.key}"
      data-name="${user.name}"
      data-email="${email}"
      data-index="name email"
    >
      <div>${user.name}</div>
      <div slot="supporting-text">${email}</div>
      <ui-user-avatar slot="start" class="avatar body-medium" .user="${user}" type="icon"></ui-user-avatar>
    </ui-list-item>`
  }

  /**
   * Renders a chip for a selected user.
   * @param user The user object to render
   */
  protected renderChip(user: IUser): TemplateResult {
    return html`<ui-chip class="chip" type="input" data-key="${user.key}" removable @remove="${this.handleChipClose}">
      <ui-user-avatar slot="avatar" class="avatar body-medium" .user="${user}" type="icon"></ui-user-avatar>
      ${user.name}
    </ui-chip>`
  }

  /**
   * Renders the grant type selector (Writer/Reader) for new shares.
   */
  protected renderGrantTypeSelector(): TemplateResult | typeof nothing {
    const hasAdds = Array.from(this.pending.values()).some((op) => op.op === 'add')
    if (!hasAdds) {
      return nothing
    }
    return html`
      <ui-select
        label="Access type"
        class="grant-type-input"
        name="grantType"
        .value="${this.grant}"
        @change="${this.handleGrantTypeChange}"
      >
        <ui-option value="writer">
          <div>Writer</div>
        </ui-option>
        <ui-option value="reader">
          <div>Reader</div>
        </ui-option>
      </ui-select>
    `
  }

  /**
   * Renders the section showing people who already have access.
   */
  protected renderPeopleWithAccess(): TemplateResult | typeof nothing {
    const hasAdds = Array.from(this.pending.values()).some((op) => op.op === 'add')
    if (this.permissions.length === 0 || hasAdds) {
      return nothing
    }
    return html`<div class="people-with-access section">
      <h3 class="title-medium">People with access</h3>
      <ui-list class="people-list" aria-label="People with access">
        ${this.permissions.map((permission) => this.renderPermission(permission))}
      </ui-list>
    </div>`
  }

  /**
   * Renders a permission item/row for an existing user.
   * @param permission The permission object
   */
  protected renderPermission(permission: IPermission): TemplateResult {
    const user = this.permissionUsers.find((u) => u.key === permission.granteeId)
    if (!user) {
      // User not found, this should generally not happen, unless the user was deleted
      // or deactivated.
      return html`<ui-list-item static class="deactivated"><div class="label">Deactivated user</div></ui-list-item>`
    }
    const email = user.email[0]?.email || ''

    const pending = this.pending.get(permission.key)
    const classes: ClassInfo = {
      'removed': pending?.op === 'remove',
      'permission-item': true,
    }

    return html`<ui-list-item lines="two" image="avatar" static class="${classMap(classes)}">
      <div class="permission-user-name">${user.name}</div>
      <div class="permission-user-email" slot="supporting-text">${email}</div>
      <ui-user-avatar slot="start" class="avatar body-medium" .user="${user}" type="icon"></ui-user-avatar>
      <div slot="end" class="permission-role">${this.renderSharedUserOptions(user, permission)}</div>
    </ui-list-item>`
  }

  /**
   * Renders the role selector for an existing permission.
   * @param user The user associated with the permission
   * @param permission The permission object
   */
  protected renderSharedUserOptions(user: IUser, permission: IPermission): TemplateResult {
    const { key } = user
    if (permission.role === 'owner') {
      // Owners cannot change their own permissions.
      // The owner permission can only be changed by transferring ownership to another user.
      return html`<span class="immutable-permission">Owner</span>`
    }
    const pending = this.pending.get(permission.key)
    let value = permission.role as string
    if (pending) {
      if (pending.op === 'remove') {
        value = 'remove'
      } else if (pending.op === 'update') {
        value = (pending as IAccessUpdateOperation).value
      }
    }
    return html`<ui-select
      class="grant-type-input"
      name="permission-${key}"
      .value="${value}"
      @change="${this.handleUserRoleChange}"
      data-key="${key}"
    >
      <ui-option value="writer">
        <div>Writer</div>
      </ui-option>
      <ui-option value="reader">
        <div>Reader</div>
      </ui-option>
      <ui-divider></ui-divider>
      <ui-option value="remove">
        <div>Remove access</div>
      </ui-option>
    </ui-select>`
  }
}
