/**
 * @file workItem.ts - API wrapper for IWS5 work item objects and functions.
 * Also includes logic for displaying work item rows in DataTable.
 */

import type { FolderField } from './folderField.ts';
import { assembleQueryParams, FetchError } from '../utils/fetchHelper.ts';
import { getAccessToken } from '../services/authService.ts';
import type { ColumnDef } from '@tanstack/react-table';
import { StatusCodes } from 'http-status-codes';
import type { Document } from './document.ts';
import type { Note } from './note.ts';
import type { DocumentTemplateField } from './documentTemplate.ts';
import { FieldInstanceTypes } from '../types/fieldInstance.ts';
import i18next from 'i18next';

/** Base URL for the IWS5 work items API endpoint */
export const WORK_ITEMS_API_URL =
  window.__APP_CONFIG__?.IWS5_API_URL + '/api/WorkItem';

/**
 * Represents a work item as returned by the IWS5 API.
 * Work items are the primary workflow entities that move through processes and queues, and they contain documents, folder fields, notes, and tracking history.
 */
export type WorkItem = {
  /** Unique reference identifier used across API calls */
  workItemReferenceId: number;

  /** Internal work item ID (may differ from reference ID); used for some API calls */
  workItemId?: number;

  /** ID of the process this work item belongs to; only returned for inbox work items */
  processId?: number;

  /** Name of the process this work item belongs to; only returned for inbox work items */
  processName?: string;

  /** ID of the queue this work item currently resides in */
  queueId?: number;

  /** Name of the queue this work item currently resides in */
  queueName: string;

  /** Username of the user who has locked this work item; null if unlocked */
  lockedBy: string | null;

  /** User-defined description of the work item; AKA the work item's name */
  description: string;

  /** ISO date string of when the work item was created */
  created: string;

  /** Username of the user who created the work item */
  creator: string;

  /** Username of the user who last forwarded/sent this work item; null if never sent */
  sentBy: string | null;

  /** Username of the user who signed this work item; null if unsigned */
  signedBy?: string | null;

  /** Username of the user this work item is assigned to; null if unassigned */
  assignedTo?: string | null;

  /** List of folder fields attached to this work item */
  fieldList?: FolderField[];
};

/**
 * String-coerced variant of {@link WorkItem} used for DataTable display.
 * All keys are converted to string-compatible types so that they render consistently in the table UI.
 * Extends Record<string, string> to allow dynamic folder field columns to be added as key-value pairs.
 */
export type WorkItemTableData = {
  [K in keyof WorkItem]: WorkItem[K] | string;
} & Record<string, string>; // Allow any extra string properties

/**
 * Transforms an array of work item objects and replaces members with formatted strings for table display.
 *
 * @param workItems Array of type WorkItem to be transformed
 * @returns Transformed WorkItemTableData objects
 */
export function transformWorkItems(workItems: WorkItem[]): WorkItemTableData[] {
  const transformedWorkItems = structuredClone(
    workItems
  ) as WorkItemTableData[];

  transformedWorkItems.forEach((transformed) => {
    transformed.created = new Date(transformed.created).toLocaleDateString();

    // Include folder fields as key/value pairs
    workItems
      .find(
        (original) =>
          original.workItemReferenceId ===
          Number(transformed.workItemReferenceId)
      )
      ?.fieldList?.forEach((field) => {
        transformed[field.fieldName] =
          field.dataType === FieldInstanceTypes.Number &&
          !Number.isNaN(field.fieldValue)
            ? parseFloat(field.fieldValue).toPrecision()
            : field.fieldValue;
      });
  });

  return transformedWorkItems;
}

/**
 * Builds and returns the default ColumnDef array for the WorkItemTableData type, for use with DataTable.
 * Includes localized headers.
 *
 * @returns Array of ColumnDef
 */
export function getDefaultWorkItemColumns(): ColumnDef<WorkItemTableData>[] {
  const tKey = 'api.work_item';

  return [
    {
      accessorKey: 'workItemReferenceId',
      header: i18next.t(tKey + '.table_columns.headers.work_item_reference_id'),
      enableColumnFilter: false,
    },
    {
      accessorKey: 'workItemId',
      header: i18next.t(tKey + '.table_columns.headers.work_item_id'),
      enableColumnFilter: false,
    },
    {
      accessorKey: 'processId',
      header: i18next.t(tKey + '.table_columns.headers.process_id'),
      enableColumnFilter: false,
    },
    {
      accessorKey: 'processName',
      header: i18next.t(tKey + '.table_columns.headers.process_name'),
      sortDescFirst: false,
      filterFn: 'equalsString',
    },
    {
      accessorKey: 'queueId',
      header: i18next.t(tKey + '.table_columns.headers.queue_id'),
      enableColumnFilter: false,
    },
    {
      accessorKey: 'queueName',
      header: i18next.t(tKey + '.table_columns.headers.queue_name'),
      sortDescFirst: false,
      filterFn: 'equalsString',
    },
    {
      accessorKey: 'lockedBy',
      header: i18next.t(tKey + '.table_columns.headers.locked_by'),
      sortDescFirst: false,
      filterFn: 'equalsString',
    },
    {
      accessorKey: 'description',
      header: i18next.t(tKey + '.table_columns.headers.description'),
      enableSorting: false,
      enableColumnFilter: false,
    },
    {
      accessorKey: 'created',
      header: i18next.t(tKey + '.table_columns.headers.created'),
      sortDescFirst: false,
      enableColumnFilter: false,
    },
    {
      accessorKey: 'creator',
      header: i18next.t(tKey + '.table_columns.headers.creator'),
      sortDescFirst: false,
      filterFn: 'equalsString',
    },
    {
      accessorKey: 'sentBy',
      header: i18next.t(tKey + '.table_columns.headers.sent_by'),
      sortDescFirst: false,
      filterFn: 'equalsString',
    },
    {
      accessorKey: 'signedBy',
      header: i18next.t(tKey + '.table_columns.headers.signed_by'),
      sortDescFirst: false,
      filterFn: 'equalsString',
    },
    {
      accessorKey: 'assignedTo',
      header: i18next.t(tKey + '.table_columns.headers.assigned_to'),
      sortDescFirst: false,
      filterFn: 'equalsString',
    },
  ];
}

/**
 * Retrieves the list of work items for a given queue.
 *
 * @param queueId ID of queue to retrieve work items for
 * @returns List of work items
 * @throws FetchError If response is not 200-class
 */
export async function getQueueWorkItems(queueId: number): Promise<WorkItem[]> {
  const _url = assembleQueryParams(WORK_ITEMS_API_URL, {
    queueId,
  });

  // Attempt GET /api/WorkItem
  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 WorkItems',
        response
      );
    }
    if (response.status === StatusCodes.NO_CONTENT) {
      return [] as WorkItem[];
    }

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

/**
 * Retrieves the list of work items for the current user's inbox.
 *
 * @returns List of work items
 * @throws FetchError If response is not 200-class
 */
export async function getInboxWorkItems(): Promise<WorkItem[]> {
  const _url = WORK_ITEMS_API_URL + '/inbox';

  // Attempt GET /api/WorkItem/inbox
  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 WorkItems',
        response
      );
    }
    if (response.status === StatusCodes.NO_CONTENT) {
      return [] as WorkItem[];
    }

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

/**
 * Checks if a work item is locked for the current user. Returns a RangeError if the work item doesn't exist.
 *
 * @param workItemReferenceId ID of work item to check lock status for
 * @returns Whether work item is locked (true/false) or RangeError if work item doesn't exist
 */
export async function getIsWorkItemUnlocked(
  workItemReferenceId: number
): Promise<boolean | RangeError> {
  const _url =
    WORK_ITEMS_API_URL + '/' + workItemReferenceId + '/isWorkitemLocked';

  // Attempt GET /api/WorkItem/{workItemReferenceId}/isWorkitemLocked
  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 check WorkItem lock status',
        response
      );
    }

    return (await response.json()) as boolean;
  } catch (error) {
    const statusCode = (error as FetchError).response?.status as number;
    const message = (error as FetchError).message;

    if (
      statusCode === StatusCodes.BAD_REQUEST ||
      (statusCode === StatusCodes.INTERNAL_SERVER_ERROR &&
        message === 'An error occurred while checking work items lock.')
    ) {
      return new RangeError();
    }

    throw new FetchError((error as Error).message);
  }
}

/**
 * Renames a work item.
 *
 * @param workItemReferenceId ID of work item to rename
 * @param newName New name for work item
 * @throws FetchError If response is not 200-class
 */
export async function renameWorkItem(
  workItemReferenceId: number,
  newName: string
): Promise<void> {
  const _url = WORK_ITEMS_API_URL + '/rename';

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

/**
 * Forwards a work item.
 *
 * @param workItemReferenceId ID of work item to forward
 * @param actionFromDropDown Action name from button
 * @param relatedQueueId Queue ID to forward from
 */
export async function forwardWorkItem(
  workItemReferenceId: number,
  actionFromDropDown: string,
  relatedQueueId: number
): Promise<void> {
  const _url = WORK_ITEMS_API_URL + '/forward';

  // Attempt PATCH /api/WorkItem/forward
  try {
    const response = await fetch(_url.toString(), {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
      body: JSON.stringify({
        workItemReferenceId,
        actionFromDropDown,
        relatedQueueId,
      }),
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(errorText || 'Failed to forward WorkItem', response);
    }
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

/**
 * Locks a work item for the current user.
 *
 * @param workItemReferenceId ID of work item to lock
 * @throws FetchError If response is not 200-class
 */
export async function lockWorkItem(workItemReferenceId: number): Promise<void> {
  const _url = WORK_ITEMS_API_URL + '/' + workItemReferenceId + '/lock';

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

/**
 * Unlocks a work item for the current user.
 *
 * @param workItemReferenceId ID of work item to unlock
 * @throws FetchError If response is not 200-class
 */
export async function unlockWorkItem(
  workItemReferenceId: number
): Promise<void> {
  const _url = WORK_ITEMS_API_URL + '/' + workItemReferenceId + '/unlock';

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

/**
 * Creates a work item in the specified process.
 *
 * @param processId ID of process to create work item in
 * @param workItemName Name of new work item
 * @param folderFields Folder fields to set for work item
 * @param documents Documents to attach to work item
 * @param notes Notes to add to work item
 * @throws FetchError If response is not 200-class
 */
export async function createWorkItem(
  processId: number,
  workItemName: string,
  folderFields: Partial<FolderField>[],
  documents: Partial<
    Document & { documentFile: string; documentFields: DocumentTemplateField[] }
  >[],
  notes: Partial<Note>[]
): Promise<void> {
  const _url = WORK_ITEMS_API_URL + '/create';

  // Attempt POST /api/WorkItem/create
  try {
    const response = await fetch(_url.toString(), {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
      body: JSON.stringify({
        workItemName,
        processId,
        // Folder field objects use different property names for this endpoint
        folderFields: folderFields.map((field) => {
          return {
            folderFieldId: field.fieldId,
            folderFieldName: field.fieldName,
            folderFieldType: field.dataType,
            folderFieldValue: field.fieldValue,
          };
        }),
        documents,
        notes,
      }),
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(errorText || 'Failed to create WorkItem', response);
    }
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

/**
 * Clones a work item.
 *
 * @param workItemReferenceId ID of work item to clone
 * @param newWorkItemName New name for cloned work item
 * @param cloneTrackingRecordMessage Tracking record message for clone action
 * @throws FetchError If response is not 200-class
 */
export async function cloneWorkItem(
  workItemReferenceId: number,
  newWorkItemName: string,
  cloneTrackingRecordMessage: string
): Promise<void> {
  const _url = WORK_ITEMS_API_URL + '/clone';

  // Attempt POST /api/WorkItem/clone
  try {
    const response = await fetch(_url.toString(), {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
      body: JSON.stringify({
        workItemReferenceId,
        newWorkItemName,
        cloneTrackingRecordMessage,
      }),
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(errorText || 'Failed to clone WorkItem', response);
    }
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}
