/**
 * @file document.ts - API wrapper for IWS5 document objects and functions.
 */

import type {
  DocumentTemplate,
  DocumentTemplateField,
} from './documentTemplate.ts';
import { assembleQueryParams, FetchError } from '../utils/fetchHelper.ts';
import { getAccessToken } from '../services/authService.ts';
import { StatusCodes } from 'http-status-codes';

/** Base URL for the IWS5 documents API endpoint */
export const DOCUMENTS_API_URL =
  window.__APP_CONFIG__?.IWS5_API_URL + '/api/Document';

/**
 * Represents a document attached to a work item, as returned by the IWS5 API.
 * Documents are files (PDFs) associated with work items that can have fields set according to a document template.
 */
export type Document = {
  /** Whether the document object includes the document content bytes */
  populatingContent: boolean;

  /** Unique identifier of the document */
  documentId: number;

  /** Display name of the document (including file extension) */
  documentName: string;

  /** Order in which the document appears in the folder tree; null if unspecified */
  displayOrder: number | null;

  /** ID of the document template used for the document's fields */
  documentTemplateId: number;

  /** Name of the document template used for the document's fields */
  documentTemplateName: string;

  /** Unused/unknown property */
  contentTypeId: number;

  /** Size of the document in bytes */
  documentSize: number;

  /** External link for the document, if applicable; null otherwise */
  documentLink: string | null;

  /** Username of the user who last modified the document */
  userName: string;

  /** ISO date string of when the document was last modified */
  lastModified: string;

  /** Optional list of template fields associated with this document */
  documentTemplateFields?: DocumentTemplateField[];
};

/**
 * Retrieves the list of documents for a given work item.
 *
 * @param workItemReferenceId ID of work item to retrieve documents for
 * @param includingDocumentContent Whether to include document content (default false)
 * @returns List of documents
 * @throws FetchError If response is not 200-class
 */
export async function listDocuments(
  workItemReferenceId: number,
  includingDocumentContent: boolean = false
): Promise<Document[]> {
  const _url = assembleQueryParams(DOCUMENTS_API_URL + '/list', {
    workItemReferenceId,
    includingDocumentContent,
  });

  // Attempt GET /api/Document/list
  try {
    const response = await fetch(_url.toString(), {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(
        errorText || 'Failed to retrieve documents',
        response
      );
    }
    if (response.status === StatusCodes.NO_CONTENT) {
      return [] as Document[];
    }

    return (await response.json()).responseModel as Document[];
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

/**
 * Retrieves the content of a specific document.
 *
 * @param documentId ID of document to retrieve content for
 * @returns Base64-encoded string of document content
 * @throws FetchError If response is not 200-class
 */
export async function getDocumentContent(documentId: number): Promise<string> {
  const _url = DOCUMENTS_API_URL + '/' + documentId;

  // Attempt GET /api/Document/{documentId}
  try {
    const response = await fetch(_url.toString(), {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(
        errorText || 'Failed to retrieve document content',
        response
      );
    }

    return (await response.json()).responseModel.document as string;
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

/**
 * Renames a specific document.
 *
 * @param processId ID of process containing work item document belongs to
 * @param documentId ID of document to rename
 * @param newName New document name
 * @returns Created document object
 * @throws FetchError If response is not 200-class
 */
export async function renameDocument(
  processId: number,
  documentId: number,
  newName: string
): Promise<Document> {
  const _url = DOCUMENTS_API_URL + '/rename';

  // Attempt PATCH /api/Document/rename
  try {
    const response = await fetch(_url.toString(), {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
      body: JSON.stringify({
        processId,
        documentId,
        newName: newName.split('.').slice(0, -1).join(''),
      }),
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(errorText || 'Failed to rename document', response);
    }

    return (await response.json()).responseModel as Document;
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

/**
 * Updates the content of a specific document.
 *
 * @param processId ID of process containing work item specified by workItemId
 * @param workItemReferenceId ID of work item containing document to be updated
 * @param documentId ID of document to update content of
 * @param documentFile Base64-encoded string with document content
 * @returns Created document object
 * @throws FetchError If response is not 200-class
 */
export async function updateDocumentContent(
  processId: number,
  workItemReferenceId: number,
  documentId: number,
  documentFile: string
): Promise<Document> {
  const _url = DOCUMENTS_API_URL + '/update-doc-content';

  // Attempt PUT /api/Document/update-doc-content
  try {
    const response = await fetch(_url.toString(), {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
      body: JSON.stringify({
        processId,
        workItemReferenceId,
        documentId,
        documentFile,
      }),
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(
        errorText || 'Failed to update document content',
        response
      );
    }

    return (await response.json()).responseModel as Document;
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

/**
 * Updates the fields of a specific document.
 *
 * @param documentId ID of document to update fields for
 * @param documentTemplate Document template object containing templateId and templateName
 * @param updatedFields Document template field objects containing new values
 * @throws FetchError If response is not 200-class
 */
export async function updateDocumentFields(
  documentId: number,
  documentTemplate: DocumentTemplate,
  updatedFields: DocumentTemplateField[]
): Promise<void> {
  const _url = DOCUMENTS_API_URL + '/update-doc-fields';

  // Attempt PUT /api/Document/update-doc-fields
  try {
    const response = await fetch(_url.toString(), {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
      body: JSON.stringify({
        documentId,
        documentTemplateId: documentTemplate.templateId,
        documentTemplateName: documentTemplate.templateName,
        fieldsToUpdate: updatedFields.map((field) => {
          return {
            fieldName: field.fieldName,
            fieldType: field.fieldType,
            fieldValue: field.fieldValue,
          };
        }),
      }),
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(
        errorText || 'Failed to update document fields',
        response
      );
    }
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

/**
 * Deletes or marks a document as deleted.
 *
 * @param documentId ID of document to delete or mark as deleted
 * @param permanently Whether to delete (true) or mark as deleted (false), default true
 * @throws FetchError If response is not 200-class
 */
export async function deleteDocument(
  documentId: number,
  permanently: boolean = true
): Promise<void> {
  const _url = assembleQueryParams(DOCUMENTS_API_URL, {
    documentId,
    permanently,
  });

  // Attempt DELETE /api/Document
  try {
    const response = await fetch(_url.toString(), {
      method: 'DELETE',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(errorText || 'Failed to delete document', response);
    }
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

/**
 * Adds a new document to the system.
 *
 * @param processId ID of process containing work item specified by workItemId
 * @param workItemId ID of work item containing new document
 * @param documentTemplateId Default documentTemplateId for process
 * @param documentTemplateName Default documentTemplateName for process
 * @param documentName Name of new document
 * @param documentFile Base64-encoded string with document content
 * @returns Created document object
 * @throws FetchError If response is not 200-class
 */
export async function addDocument(
  processId: number,
  workItemId: number,
  documentTemplateId: number,
  documentTemplateName: string,
  documentName: string,
  documentFile: string
): Promise<Document> {
  const _url = DOCUMENTS_API_URL + '/add-document';

  // Attempt POST /api/Document/add-document
  try {
    const response = await fetch(_url.toString(), {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
      body: JSON.stringify({
        processId,
        workItemId,
        documentTemplateId,
        documentTemplateName,
        documentName,
        documentFile,
        displayOrder: null,
        fields: [],
      }),
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(errorText || 'Failed to add document', response);
    }

    return (await response.json()).responseModel as Document;
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}
