<?php
// Use Statements
use classes\CRUD\CRUDController;
use classes\Document;
use classes\FunctionsGateway;
use classes\Inventory;
use classes\Location;
use classes\MysqliVeu;
use classes\PartsInvestigation;
use classes\RDTResponse;


// Includes and requires
include_once __DIR__ . '/./coreInit.php';

// ----------------------------------------------------------------------------
// Global Variables Statements and function loader call
// ----------------------------------------------------------------------------

$function = $_GET['function'] ?? '';
$postedData = json_decode((file_get_contents('php://input')), true) ?? [];

// Functions Loader
FunctionsGateway::call($function, $postedData);

// ----------------------------------------------------------------------------
// Local Helper Functions
// ----------------------------------------------------------------------------

/**
 * Retrieves the cost of an item based on the provided part number.
 *
 * @param string $partNumber The part number of the item to retrieve the cost for.
 * @param MysqliVeu $database The database connection object used to query the item cost.
 * @return float|null The cost of the item if found, or null if the item does not exist.
 */
function getItemCost(string $partNumber, MysqliVeu $database): ?float
{
    $sql = 'SELECT item_cost FROM part WHERE part_number = ?';
    $result = $database->queryParamsRow($sql, [$partNumber]);

    return (float) $result['item_cost'];
}

/**
 * Retrieves inventory records for a specified part number that match certain inventory status IDs.
 *
 * @param string $partNumber The part number for which inventory records are being retrieved.
 * @param MysqliVeu $database The database connection used for executing the query.
 * @return array An array of inventory records matching the given part number and status IDs.
 */
function getInventoryRecordsForPartNumber(string $partNumber, MysqliVeu $database): array
{
    $sql = 'SELECT *
        FROM inventory_master
        WHERE
            part_number = ?
            AND inventory_status_id IN (?, ?, ?, ?)';

    $params = [
        $partNumber,
        Inventory::getStatusIdFromStatusCode('ok', $database),
        Inventory::getStatusIdFromStatusCode('missing', $database),
        Inventory::getStatusIdFromStatusCode('pi', $database),
        Inventory::getStatusIdFromStatusCode('unpickable', $database)
    ];

    return $database->queryParamsArray($sql, $params);
}

// ----------------------------------------------------------------------------
// Main Functions
// ----------------------------------------------------------------------------

/**
 * Retrieves and processes a list of part numbers and their corresponding allocation IDs from the database,
 * based on the provided input data. It optionally returns the data in dropdown format or as a specific string value.
 *
 * @param array $postedData An associative array containing input data. Keys include:
 *                          - 'partNumber' (optional): If provided, filters the database query by this part number.
 *                          - 'responseType' (required): Specifies the response type, either 'drop' (for dropdown)
 *                            or other values for a single value response.
 * @return void Outputs the response directly through a response handler, either in dropdown format or a single
 *              concatenated string value.
 * @throws Exception
 */
function getPiPartList(array $postedData): void
{
    $database = new MysqliVeu();

    $data = [];

    // Requests data from database conditionally
    if (isset($postedData['partNumber']))
    {
        $sql = "SELECT
				MAX(pi_header.id) AS pi_header_id,
				pi_header.part_number AS part_number
			FROM pi_header
			LEFT JOIN document_status ON document_status_id = document_status.id
			WHERE
				part_number = ? AND 
				LOWER(document_status_code) IN ('open', 'complete')
			GROUP BY pi_header.part_number, pi_header.date_created
			ORDER BY pi_header.date_created DESC";

        $data = $database->queryParamsArray($sql, $postedData['partNumber']);
    }
    else
    {
        $sql = "SELECT
				MAX(pi_header.id) AS pi_header_id,
				pi_header.part_number AS part_number
			FROM pi_header
			LEFT JOIN document_status ON document_status_id = document_status.id
			WHERE 
				part_number <> '' AND
				LOWER(document_status_code) IN ('open', 'complete')
			GROUP BY pi_header.part_number, pi_header.date_created
			ORDER BY pi_header.date_created DESC";

        $data = $database->fetchArray($sql);
    }

    // Prepare dropdown data array
    $dropdownArray = [];
    foreach ($data as $row)
    {
        $dropdownArray[] = [
            'value' => "allocationId:{$row['pi_header_id']},partNumber:{$row['part_number']}",
            'text' => $row['part_number']
        ];
    }

    // Sends response conditionally
    if ($postedData['responseType'] === 'drop')
    {
        RDTResponse::dropdown($dropdownArray);
    }
    else
    {
        RDTResponse::data("{$dropdownArray[0]['value']}");
    }
}

/**
 * Initialises parts investigation (PI) tables for a given part number.
 * This method handles validations, creates or updates records in PI header, body, and detail tables,
 * and ensures proper synchronization of inventory information.
 *
 * @param array $postedData Associative array containing required data such as 'partNumber' and 'currentUser'.
 *                          'partNumber' is used to identify the inventory data to be processed.
 *                          'currentUser' represents the user initiating the operation.
 *
 * @return void No value is returned by this method. The operation results in various database updates
 *              and may trigger a success or failure response.
 * @throws Exception
 */
function initialisePiTables(array $postedData): void
{
    $database = new MysqliVeu();

    // Posted Data Validations
    if (empty($postedData['partNumber']))
    {
        RDTResponse::fail('No part number provided');
    }

    // Creates a PI object instance for part number
    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPartNumber($postedData['partNumber']);
    $piHeaderDetails = $pi->getPiHeaderDetails();

    // Checks if pi header record was already updated
    if (((int) $piHeaderDetails['actual_case_qty']) !== 0 || ((int) $piHeaderDetails['serial_not_exist']) !== 0)
    {
        RDTResponse::data("piReference:{$piHeaderDetails['pi_reference']}");
    }

    // Gets full inventory for provided part number
    $inventoryRecords = getInventoryRecordsForPartNumber($postedData['partNumber'], $database);

    // Creates confirm panel if no inventory was found for part number
    if (empty($inventoryRecords))
    {
        // Updates PI Header before closing document
        $piHeaderUpdateData = [
            'actual_case_qty' => 0,
            'expected_case_qty' => 0,
            'difference' => 0,
            'serial_not_exist' => 0,
            'incorrect_location' => 0,
            'varience_value' => 0
        ];

        $pi->updatePiHeader($postedData['currentUser'], $piHeaderUpdateData);
        $piHeaderDetails = $pi->getPiHeaderDetails();

        $yesAction = "piTransactions:closePiDocument?piReference={$piHeaderDetails['pi_reference']},changetran:PIPARTLOC";
        $noAction = "changetran:PIPARTLOC";

        RDTResponse::confirmPanel("No inventory found. Close Doc.?", $yesAction, $noAction);
    }

    // Gets all inventory records on locations other than STKCHK
    $nonPiInventoryRecords = array_filter($inventoryRecords, function ($inventoryRecord)
    {
        return $inventoryRecord['location_code'] !== 'STKCHK';
    });

    // Creates records on pi_body table
    foreach ($nonPiInventoryRecords as $inventoryRecord)
    {
        // Checks if the record exists on pi_body for inventory record location code
        $piBodyDetails = $pi->getPiBodyDetailsByLocationCode($inventoryRecord['location_code']);

        // Creates record on pi_body if not exists
        $piBodyId = null;
        if (empty($piBodyDetails))
        {
            $piBodyData = [
                'location_code' => $inventoryRecord['location_code'],
                'document_status_id' => Document::getDocumentStatusId('open', $database),
                'date_created' => 'NOW()',
                'created_by' => $postedData['currentUser'],
                'last_updated_date' => 'NOW()',
                'last_updated_by' => $postedData['currentUser']
            ];

            $piBodyId = $pi->createPiBodyRecord($piBodyData);
        }
        else
        {
            $piBodyId = $piBodyDetails['id'];
        }

        // Creates entry on pi_detail if not exists
        $piDetailRecord = $pi->getPiDetailRecordBySerialReference($inventoryRecord['serial_reference']);

        if (empty($piDetailRecord)) // Creates new record
        {
            $createPiDetailData = [
                'serial_reference' => $inventoryRecord['serial_reference'],
                'actual_qty' => 0,
                'expected_qty' => $inventoryRecord['inventory_qty'],
                'difference' => 0 - $inventoryRecord['inventory_qty'],
                'pi_body_id' => $piBodyId,
                'scanned_status' => 0,
                'date_created' => 'NOW()',
                'created_by' => $postedData['currentUser'],
                'last_updated_date' => 'NOW()',
                'last_updated_by' => $postedData['currentUser']
            ];

            if (empty($pi->createPiDetailRecord($createPiDetailData)))
            {
                RDTResponse::fail('Failed to create PI detail record');
            }
        }
    }

    // Updates pi_header and pi_body records with values from pi_detail table
    $pi->updatePiHeader($postedData['currentUser']);
    $pi->updateAllPiBodyRecords($postedData['currentUser']);

    // Success response
    $responseData = "piReference:{$pi->getPiReferenceCode()}";
    RDTResponse::data($responseData);
}

/**
 * Retrieves the next parts investigation (PI) location based on the provided PI reference and location code.
 * This method fetches PI body records, identifies locations with discrepancies, and determines the next
 * location to be inspected or updated.
 *
 * @param array $postedData Associative array containing required data:
 *                          - 'piReference': The PI reference used to fetch relevant PI details.
 *                          - 'locationCode' (optional): The current location code from which the next location
 *                            is determined. If not provided, the first location with discrepancies is returned.
 *
 * @return void No value is returned by this method. Upon execution, it generates a success response with
 *              the next PI location or an error response if no locations are found.
 */
function getPiLocation(array $postedData): void
{
    $database = new MysqliVeu();

    // Posted Data Validations
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }

    // Fetches PI locations from pi_body
    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPiReference($postedData['piReference']);
    $piBodyRecords = $pi->getAllPiBodyRecords();

    $piBodyRecordsWithDifferences = array_filter($piBodyRecords, function ($piBodyRecord)
    {
        return $piBodyRecord['expected_qty'] > $piBodyRecord['actual_qty'] || ((int) $piBodyRecord['expected_qty']) === 0 || $piBodyRecord['incorrect_location'] > 0 || $piBodyRecord['difference'] !== 0;
    });
    $piLocationsWithDifferences = array_column($piBodyRecordsWithDifferences, 'location_code');
    sort($piLocationsWithDifferences);

    // Creates Response Data
    $piLocation = '';
    if (!empty($postedData['locationCode']) && in_array($postedData['locationCode'], $piLocationsWithDifferences)) // Gets next location
    {
        $indexKey = array_search($postedData['locationCode'], $piLocationsWithDifferences);
        if ($indexKey !== false)
        {
            $indexKey++;
            if ($indexKey >= count($piLocationsWithDifferences))
            {
                $indexKey = 0;
            }
            $piLocation = $piLocationsWithDifferences[$indexKey];
        }
        else
        {
            RDTResponse::fail('Location not found');
        }
    }
    else // Gets first location
    {
        $piLocation = $piLocationsWithDifferences[0];
    }

    if (empty($piLocation))
    {
        RDTResponse::fail('No locations found');
    }

    $responseData = "locationHashCode:{$piLocation}";

    // Success data response
    RDTResponse::data($responseData);
}

// TODO: Check if function tempPiLocationsWithDifferences needs to be deplecated
function tempPiLocationsWithDifferences(array $postedData): void
{
    $database = new MysqliVeu();

    // Posted Data Validations
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }

    // Fetches PI locations from pi_body
    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPiReference($postedData['piReference']);
    $piLocationsWithDifferences = $pi->getPiLocationsWithDifferences();

    $locationsArray  = array_column($piLocationsWithDifferences, 'location_code');

    // Success dropdown response
    RDTResponse::dropdown($locationsArray);
}

function getPiLocationsDropdown(array $postedData): void
{
    $database = new MysqliVeu();

    // Posted Data Validations
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }

    // Fetches PI locations from pi_body
    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPiReference($postedData['piReference']);
    $piBodyRecords = $pi->getAllPiBodyRecords();

    $piBodyRecordsWithDifferences = array_filter($piBodyRecords, function ($piBodyRecord)
    {
        return $piBodyRecord['expected_qty'] > $piBodyRecord['actual_qty'] || ((int) $piBodyRecord['expected_qty']) === 0 || $piBodyRecord['incorrect_location'] > 0 || $piBodyRecord['difference'] !== 0;
    });
    $piLocationsWithDifferences = array_column($piBodyRecordsWithDifferences, 'location_code');
    sort($piLocationsWithDifferences);

    if (empty($piLocationsWithDifferences))
    {
        RDTResponse::fail('No available locations');
    }

    $dropdownArray = [];
    foreach ($piLocationsWithDifferences as $piLocation)
    {
        $dropdownArray[] = [
            'value' => "locationHashCodecontent:{$piLocation}",
            'text' => $piLocation
        ];
    }

    //Success dropdown response
    RDTResponse::dropdown($dropdownArray);
}

/**
 * Updates Parts Investigation (PI) records for a specific location.
 * This method validates input, processes inventory records, and synchronises entries in the
 * PI header, body, and detail tables based on the given PI reference and location code.
 * Additionally, it handles inventory movements and updates transaction history accordingly.
 *
 * @param array $postedData Associative array containing the following keys:
 *                          - 'piReference': The PI document reference for which records are being updated.
 *                          - 'locationCode': The location code where the PI operation is being performed.
 *                          - 'partNumber': The part number associated with the inventory being updated.
 *                          - 'currentUser': The user initiating the update operation.
 *
 * @return void No value is returned by this method. Database records are updated, inventory movements and
 *              differences are handled, and a response is triggered.
 * @throws Exception
 */
function updatePiRecordsForLocation(array $postedData): void
{
    $database = new MysqliVeu();
    $crudController = new CRUDController($database);

    // Posted Data Validations
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }
    if (empty($postedData['locationCode']))
    {
        RDTResponse::fail('No location code provided');
    }
    if (empty($postedData['partNumber']))
    {
        RDTResponse::fail('No part number provided');
    }

    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPiReference($postedData['piReference']);

    $piBodyDetails = $pi->getPiBodyDetailsByLocationCode($postedData['locationCode']);

    // Gets full inventory records for location code
    $inventoryRecords = getInventoryRecordsForPartNumber($postedData['partNumber'], $database);
    $inventoryRecordsOnLocation = array_filter($inventoryRecords, function ($inventoryRecord) use ($postedData)
    {
        return $inventoryRecord['location_code'] === $postedData['locationCode'];
    });

    // Gets details of STKCHK location
    $location = new Location($database);
    $piLocation = $location->getLocationRecordByLocationCode('stkchk');

    // Gets inventory status id for status code PI
    $piInventoryStatusId = Inventory::getStatusIdFromStatusCode('pi', $database);

    // Creates pi detail records
    foreach ($inventoryRecordsOnLocation as $inventoryRecord)
    {
        // Updates/Creates entry on pi_detail if not exists
        $piDetailRecord = $pi->getPiDetailRecordBySerialReference($inventoryRecord['serial_reference']);

        if (empty($piDetailRecord)) // Creates new record
        {
            $createPiDetailData = [
                'serial_reference' => $inventoryRecord['serial_reference'],
                'actual_qty' => 0,
                'expected_qty' => $inventoryRecord['inventory_qty'],
                'difference' => 0 - $inventoryRecord['inventory_qty'],
                'pi_body_id' => $piBodyDetails['id'],
                'scanned_status' => 0,
                'date_created' => 'NOW()',
                'created_by' => $postedData['currentUser'],
                'last_updated_date' => 'NOW()',
                'last_updated_by' => $postedData['currentUser']
            ];

            if (empty($pi->createPiDetailRecord($createPiDetailData)))
            {
                RDTResponse::fail('Failed to create PI detail record');
            }
        }
        else // Updates existing pi detail record for records with differences
        {
            if ($piDetailRecord['scanned_status'] === 0)
            {
                $updatePiDetailData = [
                    'expected_qty' => $inventoryRecord['inventory_qty'],
                    'difference' => 0 - $inventoryRecord['inventory_qty'],
                    'last_updated_date' => 'NOW()',
                    'last_updated_by' => $postedData['currentUser']
                ];
                $updatePiDetailWhereConditions = [
                    ['serial_reference', '=', $inventoryRecord['serial_reference']]
                ];

                $pi->updatePiDetailRecord($updatePiDetailData, $updatePiDetailWhereConditions);
            }
        }

        // Checks if the serial was already scanned
        if ($piDetailRecord['scanned_status'] === 0)
        {
            // Updates inventory record moving it to STKCHK location
            $inventoryRecordUpdateData = [
                'location_id' => $piLocation['id'],
                'location_code' => $piLocation['location_code'],
                'inventory_status_id' => $piInventoryStatusId,
                'last_updated' => 'NOW()',
                'last_updated_by' => $postedData['currentUser']
            ];

            $inventoryRecordUpdateWhereConditions = [
                ['serial_reference', '=', $inventoryRecord['serial_reference']],
                ['part_number', '=', $inventoryRecord['part_number']]
            ];

            $crudController->update('inventory_master', $inventoryRecordUpdateData, $inventoryRecordUpdateWhereConditions);

            // Creates transaction history record
            $transactionHistoryData = [
                'short_code' => 'PI',
                'part_number' => $inventoryRecord['part_number'],
                'serial_reference' => $inventoryRecord['serial_reference'],
                'ran_or_order' => $inventoryRecord['ran_or_order'],
                'txn_qty' => $inventoryRecord['inventory_qty'],
                'from_location_code' => $inventoryRecord['location_code'],
                'to_location_code' => $piLocation['location_code'],
                'associated_document_reference' => $postedData['piReference'],
                'customer_reference' => $postedData['piReference'],
                'date_created' => 'NOW()',
                'last_updated' => 'NOW()',
                'last_updated_by' => $postedData['currentUser'],
                'transaction_reference_code' => $postedData['currentUser']
            ];

            $crudController->create('transaction_history', $transactionHistoryData);
        }
    }

    // Deletes unscanned serials on pi_details not found on inventory
    $allSerialsOnInventory = array_column($inventoryRecords, 'serial_reference');
    $piDetailRecords = $pi->getPiDetailRecordsByPiBodyId($piBodyDetails['id']);
    $piDetailRecordsSerialReferences = array_column($piDetailRecords, 'serial_reference');

    $serialsOnPiDetailRecordsMissingOnInventory = array_diff($piDetailRecordsSerialReferences, $allSerialsOnInventory);

    if (!empty($serialsOnPiDetailRecordsMissingOnInventory))
    {
        foreach ($serialsOnPiDetailRecordsMissingOnInventory as $serialOnPiDetailRecordMissingOnInventory)
        {
            $piDetailRecord = $pi->getPiDetailRecordBySerialReference($serialOnPiDetailRecordMissingOnInventory);

            if (!empty($piDetailRecord) && $piDetailRecord['scanned_status'] === 0)
            {
                $pi->deletePiDetailRecordBySerialReference(((int) $piBodyDetails['id']), $serialOnPiDetailRecordMissingOnInventory);
            }
        }
    }

    // Updates pi_header and pi_body records with values from pi_detail table
    $pi->updatePiHeader($postedData['currentUser']);
    $pi->updateAllPiBodyRecords($postedData['currentUser']);

    // Success data response
    $responseData = "locationHashCode:{$postedData['locationCode']}";
    RDTResponse::data($responseData);
}

/**
 * Determines if stock for a given PI (Parts Investigation) reference and location code
 * is on a specific PI location. Validates the inputs, retrieves necessary data,
 * and checks inventory records to confirm stock location.
 *
 * @param array $postedData Associative array containing required data:
 *                          - 'piReference': The reference identifier for the PI document.
 *                          - 'locationCode': The code representing the specific location to check.
 *                          - 'partNumber': The part number linked to the inventory to be validated.
 *
 * @return void Does not return a value. Responds with success if the stock is not on
 *              the specified "stkchk" location, or a failure message otherwise.
 */
function isStockOnPiLocation(array $postedData): void
{
    $database = new MysqliVeu();

    // Posted Data Validations
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }
    if (empty($postedData['locationCode']))
    {
        RDTResponse::fail('No location code provided');
    }

    // Gets the pi body detail by location code
    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPiReference($postedData['piReference']);
    $piBodyDetails = $pi->getPiBodyDetailsByLocationCode($postedData['locationCode']);

    // Gets all the pi detail records for pi body id
    $piBodyDetailRecords = $pi->getPiDetailRecordsByPiBodyId($piBodyDetails['id']);

    // Creates an array of serial references from pi body detail records
    $piBodyDetailSerialReferences = array_column($piBodyDetailRecords, 'serial_reference');

    // Get all the inventory records for serial references on pi records
    $inventoryRecords = getInventoryRecordsForPartNumber($postedData['partNumber'], $database);
    $inventoryRecordsOnPostedLocation = array_filter($inventoryRecords, function ($inventoryRecord) use ($piBodyDetailSerialReferences)
    {
        return in_array($inventoryRecord['serial_reference'], $piBodyDetailSerialReferences);
    });

    // Loop through inventory records checking if record is on stkchk location
    foreach ($inventoryRecordsOnPostedLocation as $inventoryRecord)
    {
        if ($inventoryRecord['location_code'] === 'stkchk')
        {
            RDTResponse::fail('Stock is on STKCHK location');
        }
    }

    // Success response
    RDTResponse::ok('isStockOnPiLocation');
}

/**
 * Validates the provided serial reference details against inventory and parts investigation (PI) records.
 * This method ensures the serial reference exists, is scanned correctly, and updates the necessary PI details,
 * header, and body records. If issues are found, appropriate responses are generated.
 *
 * @param array $postedData Associative array containing required data:
 *                          - 'partNumber': The part number associated with the inventory and PI records.
 *                          - 'serialReference': The serial reference to be validated.
 *                          - 'locationCode': The location code to filter PI body details.
 *                          - 'piReference': The reference pointing to the PI header.
 *                          - 'currentUser': The user initiating this operation for audit purposes.
 *
 * @return void No value is returned. The method either updates the database records or generates
 *              failure or success responses based on the validation results.
 * @throws Exception
 */
function serialReferenceValidation(array $postedData): void
{
    $database = new MysqliVeu();

    // Posted Data Validations
    if (empty($postedData['partNumber']))
    {
        RDTResponse::fail('No part number provided');
    }
    if (empty($postedData['serialReference']))
    {
        RDTResponse::fail('No serial reference provided');
    }
    if (empty($postedData['locationCode']))
    {
        RDTResponse::fail('No location code provided');
    }
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }

    // Gets pi body details for location code
    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPiReference($postedData['piReference']);
    $piBodyDetails = $pi->getPiBodyDetailsByLocationCode($postedData['locationCode']);

    // Checks if serial reference exists on inventory_master
    $inventoryRecords = getInventoryRecordsForPartNumber($postedData['partNumber'], $database);
    $inventoryRecord = array_filter($inventoryRecords, function ($inventoryRecord) use ($postedData)
    {
        return strtoupper($inventoryRecord['serial_reference']) === $postedData['serialReference'];
    });

    // Saves invalid serial reference on pi_details and updated pi_body
    if (empty($inventoryRecord))
    {
        // Checks if serial is already on pi_detail table
        $piDetailRecord = $pi->getPiDetailRecordBySerialReference($postedData['serialReference']);

        if (empty($piDetailRecord))
        {
            // Creates pi_detail record for invalid serial
            $createPiDetailData = [
                'serial_reference' => $postedData['serialReference'],
                'actual_qty' => 0,
                'expected_qty' => 0,
                'difference' => 0,
                'pi_body_id' => $piBodyDetails['id'],
                'scanned_status' => 1,
                'date_created' => 'NOW()',
                'created_by' => $postedData['currentUser'],
                'last_updated_date' => 'NOW()',
                'last_updated_by' => $postedData['currentUser']
            ];
            $pi->createPiDetailRecord($createPiDetailData);

            // Updates pi_header and pi_body records with values from pi_detail table
            $pi->updatePiHeader($postedData['currentUser']);
            $pi->updateAllPiBodyRecords($postedData['currentUser']);

            // Fail message
            RDTResponse::fail('Invalid serial reference');
        }

        // Fail message for existing record
        RDTResponse::fail('Invalid serial reference already registered');
    }

    // Checks if serial reference was previously scanned if bypassScannedStatusCheck if FALSE
    $piDetailRecord = $pi->getPiDetailRecordBySerialReference($postedData['serialReference']);

    if (empty($postedData['bypassScannedStatusCheck'])) {
        if ($piDetailRecord['scanned_status'] === 1)
        {
            $yesAction = "piTransactions:serialReferenceValidation?partNumber=/partNumber/|AND|serialReference=/serialReference/|AND|piReference=/piReference/|AND|locationCode=/locationHashCodecontent/|AND|bypassScannedStatusCheck=true";
            RDTResponse::confirmPanel("Rescan serial?", $yesAction);
        }
    }

    // Gets inventory record current location code
    $inventoryCurrentLocationId = implode('', array_column($inventoryRecord, 'current_location_id'));
    $location = new Location($database);
    $currentLocationDetails = $location->getLocationRecordByLocationId(((int) $inventoryCurrentLocationId));
    $currentLocationCode = $currentLocationDetails['location_code'];

    // Success data message
    $inventoryQty = implode("", array_column($inventoryRecord, 'inventory_qty'));
    $responseData = "serialReferenceQty:{$inventoryQty},serialReference:{$postedData['serialReference']},serialReferenceCurrentLocationCode:{$currentLocationCode}";
    RDTREsponse::data($responseData);
}

/**
 * Validates the location of a serial reference within a parts investigation (PI) process.
 * This method compares the provided current location of a serial reference with its expected location
 * and determines whether further action is necessary to correct the discrepancy.
 *
 * @param array $postedData Associative array containing required information:
 *                          - 'piReference': The PI reference identifier for the ongoing process.
 *                          - 'serialReferenceCurrentLocationCode': The current location code of the serial reference.
 *                          - 'locationCode': The expected or target location code for the serial reference.
 *
 * @return void No value is returned by this method. The validation results in triggering either
 *              a confirmation panel if a mismatch is detected, or a success response.
 */
function serialReferenceLocationValidation(array $postedData): void
{
    $database = new MysqliVeu();

    // Posted Data Validations
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }
    if (empty($postedData['serialReferenceCurrentLocationCode']))
    {
        RDTResponse::fail('No current location provided');
    }
    if (empty($postedData['locationCode']))
    {
        RDTResponse::fail('No location code provided');
    }

    // Creates confirm panel for serial reference found in wrong location
    if ($postedData['serialReferenceCurrentLocationCode'] !== $postedData['locationCode'])
    {
        RDTResponse::confirmPanel('Serial in wrong location - Continue?');
    }

    // Success message
    RDTResponse::ok('serialReferenceLocationValidation');
}

/**
 * Validates the scanned quantity against the serial reference quantity in the provided data.
 * Generates appropriate responses based on the outcome of the validations.
 *
 * @param array $postedData The data containing 'scanQty' and 'serialReferenceQty' for validation.
 * @return void
 */
function scannedQuantityValidation(array $postedData): void
{
    // Posted Data Validations
    if (empty($postedData['scanQty']))
    {
        RDTResponse::fail('No scanned quantity provided');
    }
    if (empty($postedData['serialReferenceQty']))
    {
        RDTResponse::fail('No serial reference quantity provided');
    }

    // Creates confirm panel incorrect quantity
    if ($postedData['scanQty'] !== $postedData['serialReferenceQty'])
    {
        RDTResponse::confirmPanel('Incorrect QTY - Continue?');
    }

    // Success Response
    RDTResponse::ok('scannedQuantityValidation');
}

/**
 * Processes the PI count for a given serial reference based on the provided data.
 * Ensures proper validation and updates associated records in the database such as PI details, inventory, and transaction history.
 *
 * @param array $postedData The data required for processing, including keys such as:
 *                          'serialReference', 'piReference', 'locationCode', 'serialReferenceCurrentLocationCode',
 *                          'scanQty', 'serialReferenceQty', 'partNumber', 'currentUser', etc.
 * @return void
 * @throws Exception
 */
function processPiCountForSerialReference(array $postedData): void
{
    $database = new MysqliVeu();
    $crudController = new CRUDController($database);

    // Posted Data Validations
    if (empty($postedData['serialReference']))
    {
        RDTResponse::fail('No serial reference provided');
    }
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }
    if (empty($postedData['locationCode'])) // current location being visited
    {
        RDTResponse::fail('No location code provided');
    }
    if (empty($postedData['serialReferenceCurrentLocationCode'])) // original location of serial reference on Inventory Master
    {
        RDTResponse::fail('No current location provided');
    }
    if (empty($postedData['scanQty']))
    {
        RDTResponse::fail('No scanned quantity provided');
    }
    if (empty($postedData['serialReferenceQty']))
    {
        RDTResponse::fail('No serial reference quantity provided');
    }

    // Creates quantities variable
    $scanQty = (int) $postedData['scanQty'];
    $serialReferenceQty = (int) $postedData['serialReferenceQty'];

    // Creates an instance of pi with piReference
    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPiReference($postedData['piReference']);

    // Gets records from pi_body and pi_detail tables for current location
    $piBodyRecordCurrentLocation = $pi->getPiBodyDetailsByLocationCode($postedData['locationCode']);
    $piDetailRecordCurrentLocation = $pi->getPiDetailRecordBySerialReferenceAndPiBodyId(
        $postedData['serialReference'],
        $piBodyRecordCurrentLocation['id']
    );

    // Gets records from pi_body and pi_detail tables for original location
    $piBodyRecordOriginalLocation = $pi->getPiBodyDetailsByLocationCode($postedData['serialReferenceCurrentLocationCode']);
    $piDetailRecordOriginalLocation = $pi->getPiDetailRecordBySerialReferenceAndPiBodyId(
        $postedData['serialReference'],
        $piBodyRecordOriginalLocation['id']
    );

    // Updates/creates pi_detail records
    $updatePiDetailData = [];

    if ($postedData['locationCode'] !== $postedData['serialReferenceCurrentLocationCode']) // Serial reference found in wrong location
    {
        if (empty($piDetailRecordCurrentLocation)) // Creates a new record on pi_detail
        {
            $createPiDetailDataWrongLocation = [
                'serial_reference' => $postedData['serialReference'],
                'actual_qty' => $scanQty,
                'expected_qty' => 0,
                'difference' => $scanQty,
                'pi_body_id' => $piBodyRecordCurrentLocation['id'],
                'scanned_status' => 1,
                'date_created' => 'NOW()',
                'created_by' => $postedData['currentUser'],
                'last_updated_date' => 'NOW()',
                'last_updated_by' => $postedData['currentUser']
            ];

            $pi->createPiDetailRecord($createPiDetailDataWrongLocation);
        }
        else // Updates record existing on wrong location
        {
            $updatePiDetailDataWrongLocation = [
                'actual_qty' => $scanQty,
                'difference' => $scanQty - $piDetailRecordCurrentLocation['expected_qty'],
                'scanned_status' => 1,
                'last_updated_date' => 'NOW()',
                'last_updated_by' => $postedData['currentUser']
            ];
            $updatePiDetailWhereConditionsWrongLocation = ['id', '=', $piDetailRecordCurrentLocation['id']];

            $pi->updatePiDetailRecord($updatePiDetailDataWrongLocation, $updatePiDetailWhereConditionsWrongLocation);
        }

        // Updates scanned_status of PI body detail records for serial reference on original location
        $updatePiDetailData = [
            'scanned_status' => 1,
            'last_updated_date' => 'NOW()',
            'last_updated_by' => $postedData['currentUser']
        ];
    }
    else // Serial reference found in original location
    {
        // Updates pi_detail record
        $updatePiDetailData = [
            'actual_qty' => $scanQty,
            'difference' => $scanQty - $piDetailRecordOriginalLocation['expected_qty'],
            'scanned_status' => 1,
            'last_updated_date' => 'NOW()',
            'last_updated_by' => $postedData['currentUser']
        ];
    }

    $updatePiDetailWhereConditions = ['id', '=', $piDetailRecordOriginalLocation['id']];
    $pi->updatePiDetailRecord($updatePiDetailData, $updatePiDetailWhereConditions);

    // Updates pi_header and pi_body records with values from pi_detail table
    $pi->updatePiHeader($postedData['currentUser']);
    $pi->updateAllPiBodyRecords($postedData['currentUser']);

    // Gets inventory record for serial reference
    $sql = "SELECT *
        FROM inventory_master
        WHERE 
            serial_reference = ?
            AND part_number = ?";
    $params = [$postedData['serialReference'], $postedData['partNumber']];
    $inventoryRecord = $database->queryParamsRow($sql, $params);

    // Gets location id from location code
    $location = new Location($database);
    $locationDetails = $location->getLocationRecordByLocationCode($postedData['locationCode']);

    // Updates inventory  record
    $inventoryRecordUpdateData = [
        'hold_qty' => $inventoryRecord['inventory_qty'] - $postedData['scanQty'],
        'available_qty' => $postedData['scanQty'],
        'current_location_id' => $locationDetails['id'],
        'location_id' => $locationDetails['id'],
        'location_code' => $locationDetails['location_code'],
        'inventory_status_id' => Inventory::getStatusIdFromStatusCode('ok', $database),
        'last_updated' => 'NOW()',
        'last_updated_by' => $postedData['currentUser']
    ];

    $inventoryRecordUpdateWhereConditions = [['id', '=', $inventoryRecord['id']]];
    $crudController->update('inventory_master', $inventoryRecordUpdateData, $inventoryRecordUpdateWhereConditions);

    // Creates transaction history record
    $transactionHistoryData = [
        'short_code' => 'PI',
        'part_number' => $inventoryRecord['part_number'],
        'serial_reference' => $inventoryRecord['serial_reference'],
        'ran_or_order' => $inventoryRecord['ran_or_order'],
        'txn_qty' => $inventoryRecord['inventory_qty'],
        'from_location_code' => $inventoryRecord['location_code'],
        'to_location_code' => $postedData['locationCode'],
        'associated_document_reference' => $postedData['piReference'],
        'customer_reference' => $postedData['piReference'],
        'date_created' => 'NOW()',
        'last_updated' => 'NOW()',
        'last_updated_by' => $postedData['currentUser'],
        'transaction_reference_code' => $postedData['currentUser']
    ];
    $crudController->create('transaction_history', $transactionHistoryData);

    // Success response
    RDTResponse::ok('processPiCountForSerialReference');
}

/**
 * Checks if the provided document has unscanned serials and initiates appropriate actions.
 *
 * This method validates the provided data, fetches header details for the given piReference, and checks if there
 * are unscanned serials associated with the document. If unscanned records are found, a confirmation panel is created.
 * If no errors occur, a success response is sent.
 *
 * @param array $postedData Associative array containing input data. Must include a 'piReference' key.
 * @return void Method performs actions but does not return any value.
 */
function hasDocumentUnscannedSerials(array $postedData): void
{
    $database = new MysqliVeu();

    // Posted data Validations
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }

    // Creates an instance of pi with piReference
    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPiReference($postedData['piReference']);

    // Creates a confirmation panel if document has unscanned records on pi_details table
    if ($pi->hasPiDetailUnscannedRecords())
    {
        $yesAction = "piTransactions:hasPiHeaderDifferences?piReference=/piReference/";
        RDTResponse::confirmPanel('Doc. has unscanned serials - Continue?', $yesAction);
    }

    // Success response
    RDTResponse::ok('hasDocumentUnscannedSerials');
}

/**
 * Checks if there are differences in the PI header and initiates appropriate actions.
 *
 * This method validates the provided data, fetches PI header details for the given piReference, and checks for
 * any differences in the header information. If differences are found, a confirmation panel is created.
 * If no errors occur, a success response is sent.
 *
 * @param array $postedData Associative array containing input data. Must include a 'piReference' key.
 * @return void Method performs actions but does not return any value.
 */
function hasPiHeaderDifferences(array $postedData): void
{
    $database = new MysqliVeu();

    // Posted data Validations
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }

    // Creates an instance of pi with piReference
    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPiReference($postedData['piReference']);

    $piHeaderDetails = $pi->getPiHeaderDetails();

    // Creates a confirmation panel if there are differences
    if (((int) $piHeaderDetails['difference']) !== 0)
    {
        $yesAction = "piTransactions:closePiDocument?piReference=/piReference/,changetran:PIPARTLOC";
        RDTResponse::confirmPanel('Document has differences - Continue?', $yesAction);
    }

    // Success response
    RDTResponse::ok('hasPiHeaderDifferences');
}

/**
 * Closes a Parts Investigation (PI) document based on its details and updates its status accordingly.
 *
 * This method validates the provided data, fetches header details for the given piReference, and determines the
 * appropriate status update based on whether the document has differences. If differences exist, it calculates
 * variance value; otherwise, it marks the document as closed. A response message indicates the outcome.
 *
 * @param array $postedData Associative array containing input data. Must include a 'piReference' key and 'currentUser' for updates.
 * @return void Method performs actions but does not return any value.
 * @throws Exception
 */
function closePiDocument(array $postedData): void
{
    $database = new MysqliVeu();

    // Posted data Validations
    if (empty($postedData['piReference']))
    {
        RDTResponse::fail('No PI reference provided');
    }

    // Creates an instance of pi with piReference and gets pi header details
    $pi = new PartsInvestigation($database);
    $pi->fetchPiHeaderDetailsByPiReference($postedData['piReference']);
    $piHeaderDetails = $pi->getPiHeaderDetails();

    // Updates PI header conditionally
    $responseMessage = '';
    $piHeaderUpdateData = [];

    if (((int) $piHeaderDetails['difference']) !== 0) // Document with differences
    {
        $piHeaderUpdateData['document_status_id'] = Document::getDocumentStatusId('complete', $database);

        $itemCost = getItemCost($piHeaderDetails['part_number'], $database);

        if (empty($itemCost))
        {
            $responseMessage = 'No item cost found';
        }
        else
        {
            $varienceValue = $piHeaderDetails['difference'] * $itemCost;
            $piHeaderUpdateData['varience_value'] = round($varienceValue, 2);
            $responseMessage = 'Document completed with differences';
        }
    }
    else // Document without differences
    {
        $piHeaderUpdateData = [
            'document_status_id' => Document::getDocumentStatusId('closed', $database),
            'varience_value' => 0
        ];

        $responseMessage = 'Document closed';
    }

    $pi->updatePiHeader($postedData['currentUser'], $piHeaderUpdateData);

    RDTResponse::okMessage($responseMessage);
}
