import { nanoid } from 'nanoid'
import { IPermission, Permission, PermissionRole } from './Permission.js'
import { IModification } from './Modification.js'
import { IDeletion } from './Deletion.js'
import { IUser, Kind as UserKind } from './User.js'
import { IThing, Thing } from '../Thing.js'
import { ICapabilities } from './Capabilities.js'
import { FolderKind } from '../kinds.js'
// this causes error
// import { Kind as FolderKind } from '../Folder.js';

export interface FileAddOptions {
  /**
   * The parent folder where to put the file.
   * The `parents` array is always cleared from the file object before adding it to the store.
   */
  parent?: string
}

export interface MediaReadOptions {
  /**
   * When set it reads the file that was marked as `_deleted`. By default it throws when file was deleted.
   */
  deleted?: boolean
}

/**
 * A breadcrumb when creating the breadcrumb list for a file.
 */
export interface FileBreadcrumb {
  /**
   * The datastore key of the object.
   */
  key: string
  /**
   * THe kind of the object.
   */
  kind: string
  /**
   * The name of the object.
   */
  name: string
}

export interface IStoredFile {
  /**
   * The kind of the File
   */
  kind: string
  /**
   * The identifier of the entity
   */
  key: string
  /**
   * The parent organization of the file.
   */
  organization: string
  /**
   * The file's meta info.
   */
  info: IThing
  /**
   * The owner of this object. The id of the User object.
   * Once the File is persistent by the API this is always populated.
   */
  owner?: string
  /**
   * The color of the icon to render for this file in the file explorer.
   * This should be a hex format, e.g.: #c00 for red.
   */
  iconColor?: string
  /**
   * Whether the file is a shortcut to another file.
   * Many properties cannot be changed of a shortcut, like, shortcuts cannot be shared.
   */
  isShortcut?: boolean
  /**
   * The ID of the target file.
   */
  shortcutTarget?: string
}

/**
 * An interface describing an object in the data store that
 * describes a file or an object that can be treated as a file or a folder.
 */
export interface IFile extends IStoredFile {
  /**
   * The list of parents of the object. It is an ordered list of parents
   * from the top (first element) to the lowest parent in the tree (last element).
   *
   * After creating the object, this property cannot be manipulated directly by the client.
   * Should be treated as opaque value.
   */
  parents: string[]
  /**
   * The list of permissions to this file object.
   *
   * This property cannot be manipulated directly by the client. Should be treated as
   * opaque value.
   */
  permissionIds: string[]
  /**
   * Whether the file object is deleted.
   */
  deleted?: boolean
  /**
   * The information about the delete information.
   * Always set when the `delete` is true.
   */
  deletedInfo?: IDeletion
  /**
   * The last modification made to this file.
   */
  lastModified: IModification
  /**
   * An arbitrary list of labels applied to the file.
   */
  labels?: string[]
  /**
   * This is populated when reading a file from the store.
   * A list of actions the user can perform on the file.
   *
   * This is a readonly field and it is ignored when creating / updating the file.
   */
  capabilities?: ICapabilities
  /**
   * Populated by the server when reading the file. The list of permissions to the object.
   *
   * This property cannot be manipulated directly by the client. Should be treated as
   * opaque value.
   *
   * Data store implementation note, this is not stored in the store but it is populated
   * when reading the object.
   */
  permissions: IPermission[]
  /**
   * Populated by the store in some situations. If it's missing then you should request
   * breadcrumbs from the store.
   */
  breadcrumb?: FileBreadcrumb[]
}

const parentsSymbol = Symbol('parents')
const deletedSymbol = Symbol('deleted')
const deletedInfoSymbol = Symbol('deletedInfo')
const ownerSymbol = Symbol('owner')
const lastModifiedSymbol = Symbol('lastModified')
const capabilitiesSymbol = Symbol('capabilities')
const permissionsSymbol = Symbol('permissions')
const permissionIdsSymbol = Symbol('permissionIds')
const shortcutTargetSymbol = Symbol('shortcutTarget')

export class StoredFile {
  /**
   * The kind of the File
   */
  kind = ''
  /**
   * The identifier of the entity
   */
  key = ''
  /**
   * The parent organization of the file.
   */
  organization = ''
  /**
   * The name of the environment.
   */
  info: Thing = Thing.fromName('');

  [ownerSymbol]?: string

  /**
   * The owner of this file. This is the id of the User object.
   * Set to `default` when there are no users in the system (no authentication).
   */
  get owner(): string | undefined {
    return this[ownerSymbol]
  }

  /**
   * The color of the icon to render for this file in the file explorer.
   */
  iconColor?: string

  /**
   * Whether the file is a shortcut to another file.
   * Many properties cannot be changed of a shortcut, like, shortcuts cannot be shared.
   */
  get isShortcut(): boolean {
    return !!this.shortcutTarget
  }

  [shortcutTargetSymbol]?: string

  /**
   * The ID of the target file.
   */
  get shortcutTarget(): string | undefined {
    return this[shortcutTargetSymbol]
  }

  set shortcutTarget(value: string | undefined) {
    this[shortcutTargetSymbol] = value
  }

  new(init: IStoredFile): this {
    const { key = nanoid(), info, kind, owner, iconColor, organization, shortcutTarget } = init
    this.key = key
    this.organization = organization
    this.kind = kind
    this[ownerSymbol] = owner
    if (info) {
      this.info = new Thing(info)
    } else {
      this.info = Thing.fromName('')
    }
    if (iconColor) {
      this.iconColor = iconColor
    } else {
      this.iconColor = undefined
    }
    if (shortcutTarget) {
      this.shortcutTarget = shortcutTarget
    } else {
      this.shortcutTarget = undefined
    }
    return this
  }

  toJSON(): IStoredFile {
    const { owner } = this
    const result: IStoredFile = {
      key: this.key,
      organization: this.organization,
      kind: this.kind,
      info: this.info.toJSON(),
      owner,
    }
    if (this.iconColor) {
      result.iconColor = this.iconColor
    }
    if (this.shortcutTarget) {
      result.isShortcut = true
      result.shortcutTarget = this.shortcutTarget
    }
    return result
  }

  /**
   * Creates the Capabilities object for a file giving user level.
   *
   * @param file The file object to create the capabilities to. The object is not mutated.
   * @param role The user role to the file.
   */
  static createFileCapabilities(file: IStoredFile | StoredFile, role?: PermissionRole): ICapabilities {
    const isCommenter = Permission.hasRole('commenter', role)
    const isOwner = Permission.hasRole('owner', role)
    const isReader = Permission.hasRole('reader', role)
    const isWriter = Permission.hasRole('writer', role)
    const isFolder = file.kind === FolderKind
    const result: ICapabilities = {
      canReadMedia: !isFolder && isReader,
      canListChildren: isFolder && isReader,
      canComment: !isFolder && isCommenter,
      canEditMedia: !isFolder && isWriter,
      canEditMeta: isWriter,
      canReadRevisions: isWriter,
      canAddChildren: isFolder && isWriter,
      canDelete: isOwner,
      canTrash: isOwner,
      canUntrash: isOwner,
      canShare: isWriter,
      canMove: isWriter,
    }
    return result
  }

  createFileCapabilities(role: PermissionRole): ICapabilities {
    return File.createFileCapabilities(this, role)
  }

  /**
   * Mutates the file object by setting the `byMe` properties (on deleted and modified info)
   *
   * Note, this can be done only on file schema (IFile). The `File` object has
   * this properties frozen.
   *
   * @param file The file to mutate
   * @param user The user key to compare.
   */
  static updateByMeMeta(file: IFile, user: string): void {
    if (file.deletedInfo) {
      file.deletedInfo.byMe = file.deletedInfo.user === user
    }
    if (file.lastModified) {
      file.lastModified.byMe = file.lastModified.user === user
    }
  }

  /**
   * Static method to set the `lastModified` property on a file without creating an instance of a file meta.
   * Useful when the meta kind is unknown.
   *
   * @param file The file to update. Note, this method mutates the file.
   * @param user The user that modifies the file.
   * @returns The same file.
   */
  static setLastModified(file: IFile, user: IUser): IFile {
    if (!file) {
      throw new Error(`The file is required.`)
    }
    if (!user) {
      throw new Error(`The user is required.`)
    }
    if (user.kind !== UserKind) {
      throw new Error(`Invalid value for the user when setting "lastModified".`)
    }
    file.lastModified = {
      byMe: false,
      time: Date.now(),
      user: user.key,
      name: user.name,
    }
    return file
  }

  /**
   * Similar to the instance method `setDeleted()` but without creating
   * an instance of the file when the type is unknown.
   *
   * @param file The file to change. Note, this operation mutates the file.
   * @param user The deleting user
   * @returns The same file reference.
   */
  static setDeleted(file: IFile, user: IUser): IFile {
    if (!file) {
      throw new Error(`The file is required.`)
    }
    if (!user) {
      throw new Error(`The user is required.`)
    }
    if (user.kind !== UserKind) {
      throw new Error(`Invalid value for the user when setting "deletedInfo".`)
    }
    file.deleted = true
    file.deletedInfo = {
      byMe: false,
      time: Date.now(),
      user: user.key,
      name: user.name,
    }
    return file
  }
}

export class File extends StoredFile {
  [parentsSymbol]: string[] = []

  /**
   * The list of parents of the object. It is an ordered list of parents
   * from the top (first element) to the lowest parent in the tree (last element).
   *
   * This property cannot be manipulated directly by the client. Should be treated as
   * opaque value.
   */
  get parents(): string[] {
    return this[parentsSymbol]
  }

  [permissionIdsSymbol]: readonly string[] = []

  /**
   * The list of permissions to this file object.
   *
   * This property cannot be manipulated directly by the client. Should be treated as
   * opaque value.
   */
  get permissionIds(): readonly string[] {
    return this[permissionIdsSymbol]
  }

  [deletedSymbol]?: boolean

  /**
   * Whether the file object is deleted.
   */
  get deleted(): boolean {
    return this[deletedSymbol] || false
  }

  [deletedInfoSymbol]?: Readonly<IDeletion>

  /**
   * The information about the delete information.
   * Always set when the `delete` is true.
   */
  get deletedInfo(): Readonly<IDeletion> | undefined {
    return this[deletedInfoSymbol]
  }

  [lastModifiedSymbol]: Readonly<IModification> = { user: '', time: 0, byMe: false }

  /**
   * The last modification made to this file.
   */
  get lastModified(): Readonly<IModification> {
    return this[lastModifiedSymbol]
  }

  /**
   * An arbitrary list of labels applied to the file.
   */
  labels?: string[];

  [capabilitiesSymbol]?: Readonly<ICapabilities>

  /**
   * This is populated when reading a file from the store.
   * A list of actions the user can perform on the file.
   *
   * This is a readonly field and it is ignored when creating / updating the file.
   */
  get capabilities(): Readonly<ICapabilities> | undefined {
    return this[capabilitiesSymbol]
  }

  [permissionsSymbol]: readonly IPermission[] = []

  /**
   * Populated by the server when reading the file. The list of permissions to the object.
   *
   * This property cannot be manipulated directly by the client. Should be treated as
   * opaque value.
   */
  get permissions(): readonly IPermission[] {
    return this[permissionsSymbol]
  }

  override new(init: IFile): this {
    super.new(init)
    const { permissions = [], parents = [], permissionIds = [], deleted, deletedInfo, lastModified, labels } = init
    this[permissionsSymbol] = permissions.map((i) => ({ ...i }))
    this[parentsSymbol] = [...parents]
    this[permissionIdsSymbol] = [...permissionIds]
    this[lastModifiedSymbol] = lastModified
      ? Object.freeze({ ...lastModified })
      : Object.freeze({ user: '', time: 0, byMe: false })
    if (typeof deleted === 'boolean') {
      this[deletedSymbol] = deleted
      this[deletedInfoSymbol] = deletedInfo ? Object.freeze({ ...deletedInfo }) : undefined
    } else {
      this[deletedSymbol] = undefined
      this[deletedInfoSymbol] = undefined
    }
    if (Array.isArray(labels)) {
      this.labels = [...labels]
    } else {
      this.labels = undefined
    }
    return this
  }

  override toJSON(): IFile {
    const result: IFile = {
      ...super.toJSON(),
      parents: [...this.parents],
      permissionIds: [...this.permissionIds],
      lastModified: { ...this.lastModified },
      permissions: [...this.permissions],
    }
    if (this.deleted) {
      result.deleted = this.deleted
      result.deletedInfo = { ...this.deletedInfo } as IDeletion
    }
    if (Array.isArray(this.labels)) {
      result.labels = [...this.labels]
    }
    return result
  }

  /**
   * @param name The name to set.
   * @param owner The user id that is the owner of the file.
   */
  static fromName(name: string, organization: string, owner?: string, kind = ''): File {
    const key = nanoid()
    const definition = new File()
    definition.new({
      key,
      kind,
      organization,
      info: Thing.fromName(name).toJSON(),
      owner,
      parents: [],
      permissionIds: [],
      permissions: [],
      lastModified: { user: owner || '', time: 0, byMe: false },
    })
    return definition
  }

  shortcutTo(fileId: string): this {
    this.shortcutTarget = fileId
    return this
  }

  /**
   * Updates the "lastModified" value.
   * A helper method for a common task.
   *
   * @param user The user that modifies the entity.
   */
  setLastModified(user: IUser): void {
    if (!user) {
      throw new Error(`The user is required.`)
    }
    if (user.kind !== UserKind) {
      throw new Error(`Invalid value for the user when setting "lastModified".`)
    }
    this[lastModifiedSymbol] = {
      byMe: false,
      time: Date.now(),
      user: user.key,
      name: user.name,
    }
  }

  /**
   * Adds a label to the list of labels.
   *
   * It makes sure the value is a valid, non-empty string and the `labels` array is set.
   *
   * It does nothing when the label already exists.
   *
   * @param label The label to set.
   */
  addLabel(label: string): void {
    if (typeof label !== 'string') {
      throw new Error(`The label must be a string.`)
    }
    if (!label.trim()) {
      throw new Error(`The label is required.`)
    }
    if (!Array.isArray(this.labels)) {
      this.labels = []
    }
    if (this.labels.includes(label)) {
      return
    }
    this.labels.push(label)
  }

  /**
   * Marks the entity as deleted.
   *
   * A helper method for a common task.
   *
   * @param user The user that deletes the entity.
   */
  setDeleted(user: IUser): void {
    if (!user) {
      throw new Error(`The user is required.`)
    }
    if (user.kind !== UserKind) {
      throw new Error(`Invalid value for the user when setting "deletedInfo".`)
    }
    this[deletedSymbol] = true
    this[deletedInfoSymbol] = {
      byMe: false,
      time: Date.now(),
      user: user.key,
      name: user.name,
    }
  }

  isFile(): boolean {
    return this.kind !== FolderKind
  }

  isFolder(): boolean {
    return this.kind === FolderKind
  }
}
