///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2025, Open Design Alliance (the "Alliance").
// All rights reserved.
//
// This software and its documentation and related materials are owned by
// the Alliance. The software may only be incorporated into application
// programs owned by members of the Alliance, subject to a signed
// Membership Agreement and Supplemental Software License Agreement with the
// Alliance. The structure and organization of this software are the valuable
// trade secrets of the Alliance and its suppliers. The software is also
// protected by copyright law and international treaty provisions. Application
// programs incorporating this software must include the following statement
// with their copyright notices:
//
//   This application incorporates Open Design Alliance software pursuant to a
//   license agreement with Open Design Alliance.
//   Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance.
//   All rights reserved.
//
// By use of this software, its documentation or related materials, you
// acknowledge and accept the above terms.
///////////////////////////////////////////////////////////////////////////////

import { IHttpClient } from "./IHttpClient";
import { Endpoint } from "./Endpoint";
import { IShortUserDesc } from "./IUser";
import { Role } from "./Role";
import { IRoleActions } from "./IRole";
import { Member } from "./Member";
import { File } from "./File";
import { userFullName, userInitials } from "./Utils";

/**
 * Provides properties and methods for obtaining information about a project on the Open Cloud Server and
 * managing its {@link Role | roles}, {@link Member | members} and models.
 */
export class Project extends Endpoint {
  private _data: any;

  /**
   * @param data - Raw project data received from the server. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
   * @param httpClient - HTTP client instance used to send requests to the REST API server.
   */
  constructor(data: any, httpClient: IHttpClient) {
    super(`/projects/${data.id}`, httpClient);
    this.data = data;
  }

  /**
   * Project features the user has access to.
   *
   * @readonly
   */
  get authorization(): {
    /**
     * Actions are allowed to be performed:
     *
     * - `update` - The ability to update the project details.
     * - `createTopic` - The ability to create a new topic.
     * - `createDocument` - The ability to create a new document.
     */
    project_actions: string[];
  } {
    return this.data.authorization;
  }

  /**
   * Project creation time (UTC) in the format specified in
   * {@link https://www.wikipedia.org/wiki/ISO_8601 | ISO 8601}.
   *
   * @readonly
   */
  get createdAt(): string {
    return this.data.createdAt;
  }

  /**
   * Project custom fields object, to store custom data.
   */
  get customFields(): any {
    return this.data.customFields;
  }

  set customFields(value: any) {
    this.data.customFields = value;
  }

  /**
   * Raw project data received from the server. For more information, see
   * {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
   *
   * @readonly
   */
  get data(): any {
    return this._data;
  }

  private set data(value: any) {
    this._data = value;
    this._data.previewUrl = value.avatarUrl
      ? `${this.httpClient.serverUrl}/projects/${this._data.id}/preview?updated=${value.updatedAt}`
      : "";
    this._data.owner.avatarUrl = `${this.httpClient.serverUrl}/users/${this._data.owner.userId}/avatar`;
    this._data.owner.fullName = userFullName(this._data.owner);
    this._data.owner.initials = userInitials(this._data.owner.fullName);
  }

  /**
   * Project description.
   */
  get description(): string {
    return this.data.description;
  }

  set description(value: string) {
    this.data.description = value;
  }

  /**
   * Project end date in the format specified in
   * {@link https://www.wikipedia.org/wiki/ISO_8601 | ISO 8601}.
   */
  get endDate(): string {
    return this.data.endDate;
  }

  set endDate(value: string | Date) {
    this.data.endDate = value instanceof Date ? value.toISOString() : value;
  }

  /**
   * Unique project ID.
   *
   * @readonly
   */
  get id(): string {
    return this.data.id;
  }

  /**
   * The number of members in the project.
   *
   * @readonly
   */
  get memberCount(): number {
    return this.data.memberCount;
  }

  /**
   * The number of models in the project.
   *
   * @readonly
   */
  get modelCount(): number {
    return this.data.modelCount;
  }

  /**
   * Project name.
   */
  get name(): string {
    return this.data.name;
  }

  set name(value: string) {
    this.data.name = value;
  }

  /**
   * Project owner information.
   *
   * @readonly
   */
  get owner(): IShortUserDesc {
    return this.data.owner;
  }

  /**
   * Project preview image URL or empty string if the project does not have a preview. Use
   * {@link Project.setPreview | setPreview()} to change preview image.
   *
   * @readonly
   */
  get previewUrl(): string {
    return this._data.previewUrl;
  }

  /**
   * `true` if project is shared project.
   */
  get public(): boolean {
    return this.data.public;
  }

  set public(value: boolean) {
    this.data.public = value;
  }

  /**
   * Project start date in the format specified in
   * {@link https://www.wikipedia.org/wiki/ISO_8601 | ISO 8601}.
   */
  get startDate(): string {
    return this.data.startDate;
  }

  set startDate(value: string | Date) {
    this.data.startDate = value instanceof Date ? value.toISOString() : value;
  }

  /**
   * The number of topics in the project.
   *
   * @readonly
   */
  get topicCount(): number {
    return this.data.topicCount;
  }

  /**
   * Project last update time (UTC) in the format specified in
   * {@link https://www.wikipedia.org/wiki/ISO_8601 | ISO 8601}.
   *
   * @readonly
   */
  get updatedAt(): string {
    return this.data.updatedAt;
  }

  /**
   * Reloads project data from the server.
   */
  async checkout(): Promise<this> {
    const response = await this.get("");
    this.data = await response.json();
    return this;
  }

  /**
   * Updates project data on the server.
   *
   * @param data - Raw project data. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
   */
  async update(data: any): Promise<this> {
    const response = await this.put("", data);
    this.data = await response.json();
    return this;
  }

  /**
   * Deletes a project from the server.
   *
   * @returns Returns the raw data of a deleted project. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
   */
  override delete(): Promise<any> {
    return super
      .delete("")
      .then((response) => response.text())
      .then((text) => {
        // TODO fix for server 23.5 and below
        try {
          return JSON.parse(text);
        } catch {
          return { id: this.id };
        }
      });
  }

  /**
   * Saves project properties changes to the server. Call this method to update project data on the
   * server after any property changes.
   */
  save(): Promise<this> {
    return this.update(this.data);
  }

  /**
   * Sets or removes the project preview.
   *
   * @param image - Preview image. Can be a
   *   {@link https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | Data URL} string,
   *   {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer | ArrayBuffer},
   *   {@link https://developer.mozilla.org/docs/Web/API/Blob/Blob | Blob} or
   *   {@link https://developer.mozilla.org/docs/Web/API/File | Web API File} object. Setting the `image`
   *   to `null` will remove the preview.
   */

  async setPreview(image?: BodyInit | null): Promise<this> {
    if (!image) {
      await this.deletePreview();
    } else {
      const response = await this.post("/preview", image);
      this.data = await response.json();
    }
    return this;
  }

  /**
   * Removes the project preview.
   */
  async deletePreview(): Promise<this> {
    const response = await super.delete("/preview");
    this.data = await response.json();
    return this;
  }

  /**
   * Returns a list of project roles. Project members have different abilities depending on the role they
   * have in a project.
   */
  getRoles(): Promise<Role[]> {
    return this.get("/roles")
      .then((response) => response.json())
      .then((array) => array.map((data) => new Role(data, this.id, this.httpClient)));
  }

  /**
   * Returns information about the specified project role.
   *
   * @param name - Role name.
   */
  getRole(name: string): Promise<Role> {
    return this.get(`/roles/${name}`)
      .then((response) => response.json())
      .then((data) => new Role(data, this.id, this.httpClient));
  }

  /**
   * Creates a new project role.
   *
   * @param name - Role name.
   * @param description - Role description.
   * @param permissions - Actions are allowed to be performed for the role.
   */
  createRole(name: string, description: string, permissions: IRoleActions): Promise<Role> {
    return this.post("/roles", {
      name,
      description,
      permissions: permissions || {},
    })
      .then((response) => response.json())
      .then((data) => new Role(data, this.id, this.httpClient));
  }

  /**
   * Deletes the specified project role.
   *
   * @param name - Role name.
   * @returns Returns the raw data of a deleted role. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
   */
  deleteRole(name: string): Promise<any> {
    return super.delete(`/roles/${name}`).then((response) => response.json());
  }

  /**
   * Returns a list of project members.
   */
  getMembers(): Promise<Member[]> {
    return this.get("/members")
      .then((response) => response.json())
      .then((array) => array.map((data) => new Member(data, this.id, this.httpClient)));
  }

  /**
   * Returns information about the specified project member.
   *
   * @param memberId - Member ID.
   */
  getMember(memberId: string): Promise<Member> {
    return this.get(`/members/${memberId}`)
      .then((response) => response.json())
      .then((data) => new Member(data, this.id, this.httpClient));
  }

  /**
   * Adds a user to the project to become a member and have permission to perform actions.
   *
   * @param userId - User ID.
   * @param role - Role name from the list of project {@link getRoles | roles}.
   */
  addMember(userId: string, role: string): Promise<Member> {
    return this.post("/members", { userId, role })
      .then((response) => response.json())
      .then((data) => new Member(data, this.id, this.httpClient));
  }

  /**
   * Removes the specified member from a project.
   *
   * @param memberId - Member ID.
   * @returns Returns the raw data of a deleted member. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
   */
  removeMember(memberId: string): Promise<any> {
    return super.delete(`/members/${memberId}`).then((response) => response.json());
  }

  /**
   * Information about the file (model) that can be reference in the project topics.
   *
   * @typedef {any} FileInformation
   * @property {any[]} display_information - The list of fields to allow users to associate the file with
   *   a server model.
   * @property {string} display_information.field_display_name - Field display name.
   * @property {string} display_information.field_value - Field value.
   * @property {any} file - The file reference object.
   * @property {string} file.file_name - File name.
   * @property {string} file.reference - File ID.
   */

  /**
   * Returns a list of project files. For more information, see
   * {@link https://cloud.opendesign.com/docs//pages/server/bcf3.html#ProjectFilesInformation | Open Cloud BCF3 API}.
   *
   * This list contains all files that the project has access to. To add a file to this list, create a
   * {@link IGrantedTo.project | project} permission on the file using
   * {@link File.createPermission | File.createPermission()}.
   */
  getFilesInformation(): Promise<any[]> {
    const bcfProjects = new Endpoint("/bcf/3.0/projects", this.httpClient, this.headers);
    return bcfProjects
      .get(`/${this.id}/files_information`)
      .then((response) => response.json())
      .then((items) => {
        items.forEach((item) => {
          const getFieldValue = (displayName: string) => {
            return (item.display_information.find((x) => x.field_display_name === displayName) || {}).field_value;
          };

          const previewUrl = `${this.httpClient.serverUrl}/files/${item.file.reference}/preview`;
          const ownerAvatarUrl = `${this.httpClient.serverUrl}/users/${getFieldValue("Owner")}/avatar`;

          const ownerFirstName = getFieldValue("Owner First Name");
          const ownerLastName = getFieldValue("Owner Last Name");
          const ownerUserName = getFieldValue("Owner User Name");
          const ownerFullName = userFullName(ownerFirstName, ownerLastName, ownerUserName);
          const ownerInitials = userInitials(ownerFullName);

          item.display_information.push({ field_display_name: "Preview URL", field_value: previewUrl });
          item.display_information.push({ field_display_name: "Owner Avatar URL", field_value: ownerAvatarUrl });
          item.display_information.push({ field_display_name: "Owner Full Name", field_value: ownerFullName });
          item.display_information.push({ field_display_name: "Owner Initials", field_value: ownerInitials });

          // updatedBy since 24.10

          const updatedByAvatarUrl = `${this.httpClient.serverUrl}/users/${getFieldValue("Updated By")}/avatar`;

          const updatedByFirstName = getFieldValue("Updated By First Name");
          const updatedByLastName = getFieldValue("Updated By Last Name");
          const updatedByUserName = getFieldValue("Updated By User Name");
          const updatedByFullName = userFullName(updatedByFirstName, updatedByLastName, updatedByUserName);
          const updatedByInitials = userInitials(updatedByFullName);

          item.display_information.push({
            field_display_name: "Updated By Avatar URL",
            field_value: updatedByAvatarUrl,
          });
          item.display_information.push({ field_display_name: "Updated By Full Name", field_value: updatedByFullName });
          item.display_information.push({ field_display_name: "Updated By Initials", field_value: updatedByInitials });

          // geometryType since 24.12

          const geometry = getFieldValue("Geometry Status");
          const geometryGltf = getFieldValue("GeometryGltf Status");
          const geometryType = geometry === "done" ? "vsfx" : geometryGltf === "done" ? "gltf" : "";

          item.display_information.push({ field_display_name: "Geometry Type", field_value: geometryType });
        });
        return items;
      });
  }

  /**
   * Returns a list of project files.
   */
  getModels(): Promise<File[]> {
    return this.getFilesInformation()
      .then((filesInformation) => filesInformation.map((item) => item.file.reference))
      .then((ids) => {
        const files = new Endpoint("/files", this.httpClient, this.headers);
        return files.get(`?id=${ids.join("|")}`);
      })
      .then((response) => response.json())
      .then((files) => files.result.map((data) => new File(data, this.httpClient)));
  }

  /**
   * Adds a file to the project with specified permissions.
   *
   * To change file permissions for the project use {@link Permission.actions}.
   *
   * @param fileId - File ID.
   * @param actions - Actions are allowed to be performed on a file:
   *
   *   - `read` - The ability to read file description, geometry data and properties.
   *   - `readSourceFile` - The ability to download source file.
   *   - `write` - The ability to modify file name, description and references.
   *   - `readViewpoint` - The ability to read file viewpoints.
   *   - `createViewpoint` - The ability to create file viewpoints.
   *
   * @param _public - Specifies whether all users have access to the file or not.
   * @returns Returns a file instance added to the project.
   */
  async addModel(fileId: string, actions: string | string[], _public: boolean): Promise<File> {
    const files = new Endpoint("/files", this.httpClient, this.headers);
    const file = await files
      .get(`/${fileId}`)
      .then((response) => response.json())
      .then((data) => new File(data, this.httpClient));

    const grantedTo = [{ project: { id: this.id, name: this.name } }];
    await file.createPermission(actions, grantedTo, _public);

    return file;
  }

  /**
   * Removes the specified file from a project.
   *
   * @param fileId - File ID.
   * @returns Returns a file instance removed from the project.
   */
  async removeModel(fileId: string): Promise<File> {
    const files = new Endpoint("/files", this.httpClient, this.headers);
    const file = await files
      .get(`/${fileId}`)
      .then((response) => response.json())
      .then((data) => new File(data, this.httpClient));

    const permissions = await file.getPermissions();
    await Promise.allSettled(
      permissions
        .filter((permission) => permission.grantedTo.some((x) => x.project?.id === this.id))
        .map((permission) => permission.delete())
    );

    return file;
  }
}
