import { assembleQueryParams, FetchError } from '../utils/FetchHelper';
import { getStudents, getUsers } from './Users';
import { getFormattedTeamName, getTeams } from './Teams';
import { getPart, getParts } from './Parts';
import { getSuppliers } from './Suppliers';
import { APIColumnDef } from '../utils/TableHelper';
import { createNotification, Notification } from './Notifications';

// Define API endpoint for requests
export const REQUESTS_API_URL =
  location.protocol + location.host + '/api/requests';

// Define TypeScript types for requests matching ShipME API schema
// NOTE: These cannot be used like classes, they are essentially just validators for JSON objects
export type Request = {
  // Uses POST body to determine required properties
  request_id?: number;
  request_timestamp?: string;
  student_id: number;
  team_id: number;
  supplier_id: number;
  part_id: number;
  order_id?: number;
  status: string;
  quantity: number;
  subtotal: number;
};
export type RequestPatch = {
  order_id?: number | 'unset';
  status?: string;
  quantity?: number;
  subtotal?: number;
};
export type RequestTableData = {
  request_id?: number | string;
  request_timestamp?: string;
  student_id: number | string;
  team_id: number | string;
  supplier_id: number | string;
  part_id: number | string;
  order_id?: number | string;
  status: string;
  quantity: number | string;
  subtotal: number | string;
};

// Function transforms an array of Request objects and replaces members with new formatted strings for table display
// IN: requests : Request array to be transformed
// OUT: transformed RequestTableData array
export async function transformRequests(
  requests: Request[]
): Promise<RequestTableData[]> {
  // Get data for foreign key objects
  const users = await getUsers('student');
  const students = await getStudents();
  const teams = await getTeams();
  const suppliers = await getSuppliers();
  const parts = await getParts();

  requests.forEach((request: RequestTableData) => {
    request.request_timestamp = new Date(
      request.request_timestamp as string
    ).toLocaleString();
    // Set the names of foreign key references in place of their IDs
    const student = students.find(
      (student) => student.student_id === request.student_id
    );
    const user = users.find((user) => user.user_id === student?.user_id);
    if (user) {
      request.student_id = user.first_name + ' ' + user.last_name;
    }
    const team = teams.find((team) => team.team_id === request.team_id);
    if (team) {
      request.team_id = getFormattedTeamName(team, undefined, true);
    }
    const supplier = suppliers.find(
      (supplier) => supplier.supplier_id === request.supplier_id
    );
    if (supplier) {
      request.supplier_id = supplier.name;
    }
    const part = parts.find((part) => part.part_id === request.part_id);
    if (part) {
      // Uses Unicode Record Separator character to join description and URL (separated in DataTable)
      // This is the best I could come up with without a bunch of DataTable rework
      request.part_id = part.description + '\u001E' + part.url;
    }
  });

  return requests;
}

// Define ColumnDef array for Request type
export const defaultRequestColumns: APIColumnDef<
  RequestTableData,
  RequestPatch
>[] = [
  {
    accessorKey: 'request_id',
    header: 'ID',
    enableColumnFilter: false,
  },
  {
    accessorKey: 'request_timestamp',
    header: 'Timestamp',
    sortDescFirst: false,
    enableColumnFilter: false,
    meta: {
      transform: (value) => new Date(value).toLocaleString(),
    },
  },
  {
    accessorKey: 'student_id',
    header: 'Student',
    sortDescFirst: false,
    filterFn: 'equalsString',
  },
  {
    accessorKey: 'team_id',
    header: 'Team',
    sortDescFirst: false,
    filterFn: 'equalsString',
  },
  {
    accessorKey: 'supplier_id',
    header: 'Supplier',
    sortDescFirst: false,
    filterFn: 'equalsString',
  },
  {
    accessorKey: 'part_id',
    header: 'Part',
    sortDescFirst: false,
    enableColumnFilter: false,
  },
  {
    accessorKey: 'order_id',
    header: 'Order',
    sortDescFirst: false,
    filterFn: 'equalsString',
  },
  {
    accessorKey: 'status',
    header: 'Status',
    filterFn: 'equalsString',
    meta: {
      update: updateRequest,
      element: 'select',
      values: [
        'requested',
        'approved',
        'ordered',
        'delivered',
        'delayed',
        'reordered',
        'on hand',
        'cancelled',
        'other',
      ],
      restrictedValues: ['requested', 'cancelled'],
      allowedUserTypes: ['instructor', 'ta', 'student'],
    },
  },
  {
    accessorKey: 'quantity',
    header: 'Quantity',
    sortDescFirst: false,
    enableColumnFilter: false,
    meta: {
      update: updateRequest,
      type: 'number',
      element: 'input',
      allowedUserTypes: ['instructor', 'ta', 'student'],
    },
  },
  {
    accessorKey: 'subtotal',
    header: 'Subtotal ($)',
    sortDescFirst: false,
    enableColumnFilter: false,
    meta: {
      update: updateRequest,
      type: 'number',
      element: 'input',
      allowedUserTypes: ['instructor', 'ta', 'student'],
    },
  },
];

// Function makes GET request to ShipME API /api/requests endpoint and returns an array of Requests
// To use only one filter, pass undefined (the word, not a string) for the ommitted parameter
// IN: student_id : optional student_id to filter requests by
//     team_id : optional team_id to filter requests by
//     supplier_id : optional supplier_id to filter requests by
//     part_id : optional part_id to filter requests by
//     order_id : optional order_id to filter requests by
//     status : optional status to filter requests by
// OUT: array of retrieved Requests
// THROWS: FetchError
export async function getRequests(
  student_id?: number,
  team_id?: number,
  supplier_id?: number,
  part_id?: number,
  order_id?: number,
  status?: string
): Promise<Request[]> {
  const _url = assembleQueryParams(REQUESTS_API_URL, {
    student_id: student_id?.toString(),
    team_id: team_id?.toString(),
    supplier_id: supplier_id?.toString(),
    part_id: part_id?.toString(),
    order_id: order_id?.toString(),
    status: status,
  });

  // Attempt GET /api/requests
  try {
    const response = await fetch(_url.toString());
    // Catch response codes other than 200-class
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(
        errorText || 'Failed to retrieve requests',
        response
      );
    }

    // Return requests as array of type Request
    const requests: Request[] = await response.json();
    return requests;
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

// Function makes POST request to ShipME API /api/requests endpoint and returns a single Request
// IN: request : Request object with request data to be submitted
// OUT: response Request object from API
// THROWS: FetchError
export async function createRequest(request: Request): Promise<Request> {
  const _url = new URL(REQUESTS_API_URL);

  // Attempt POST /api/requests
  try {
    const response = await fetch(_url.toString(), {
      headers: {
        'Content-Type': 'application/json',
      },
      method: 'POST',
      body: JSON.stringify(request),
    });
    // Catch response codes other than 200-class
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(errorText || 'Failed to create request', response);
    }

    // Return request as type Requests
    request = await response.json();
    return request;
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

// Function makes GET request to ShipME API /api/requests/{request_id} endpoint and returns a single Request
// IN: request_id : the request_id of the request to retrieve
// OUT: retrieved Request
// THROWS: FetchError
export async function getRequest(request_id: number): Promise<Request> {
  const _url = new URL(REQUESTS_API_URL + '/' + request_id);

  // Attempt GET /api/requests/{request_id}
  try {
    const response = await fetch(_url.toString());
    // Catch response codes other than 200-class
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(errorText || 'Failed to retrieve request', response);
    }

    // Return request as type Request
    const request: Request = await response.json();
    return request;
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

// Function makes PATCH request to ShipME API /api/requests/{request_id} endpoint and returns a single Request
// IN: request_id : the request_id of the request to update
//     patchObject : RequestPatch object with optional request data to be submitted
// OUT: response Request object from API
// THROWS: FetchError
export async function updateRequest(
  request_id: number,
  patchObject: RequestPatch
): Promise<Request> {
  const _url = new URL(REQUESTS_API_URL + '/' + request_id);

  // If the request quantity is being updated but NOT the subtotal, enforce parity
  if (patchObject.quantity && !patchObject.subtotal) {
    const request = await getRequest(request_id);
    const part = await getPart(request.part_id as number);

    patchObject.subtotal = part.price * patchObject.quantity;
  }

  // Automatically unset order associated when marked as "reordered"
  if (
    patchObject.status &&
    patchObject.status === 'reordered' &&
    !patchObject.order_id
  ) {
    patchObject.order_id = 'unset';
  }

  // Attempt PATCH /api/requests/{request_id}
  try {
    const response = await fetch(_url.toString(), {
      headers: {
        'Content-Type': 'application/json',
      },
      method: 'PATCH',
      body: JSON.stringify(patchObject),
    });
    // Catch response codes other than 200-class
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(errorText || 'Failed to update request', response);
    }

    // Return request as type Request
    const request: Request = await response.json();

    // generate notification
    if (patchObject.quantity || patchObject.subtotal || patchObject.status) {
      let generatedMessage = `Request #${request.request_id} has been updated:`;
      let free_text = '';
      if (patchObject.quantity) {
        free_text += `Quantity updated to ${patchObject.quantity}.`;
      }
      if (patchObject.subtotal) {
        free_text += `Subtotal updated to $${patchObject.subtotal}.`;
      }
      if (patchObject.status) {
        free_text += `Status updated to ${patchObject.status}.`;
      }
      const newNotification: Notification = {
        request_id: request.request_id as number,
        team_id: request.team_id,
        generated_message: generatedMessage,
        free_text: free_text,
        for_instructor: false,
      };
      await createNotification(newNotification);
    }

    return request;
  } catch (error) {
    throw new FetchError((error as Error).message);
  }
}

// Function makes DELETE request to ShipME API /api/requests/{request_id} endpoint and returns a Response
// IN: request_id : the request_id of the request to delete
// OUT: Fetch Response object
// THROWS: FetchError
export async function deleteRequest(request_id: number): Promise<Response> {
  const _url = new URL(REQUESTS_API_URL + '/' + request_id);

  // Attempt DELETE /api/requests/{request_id}
  try {
    const response = await fetch(_url.toString(), {
      method: 'DELETE',
    });
    // Catch response codes other than 200-class
    if (!response.ok) {
      const errorText = await response.text();
      throw new FetchError(errorText || 'Failed to delete request', response);
    }

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