<?php

if (!function_exists('plantChangeTrackedFields')) {
    function plantChangeTrackedFields(): array
    {
        return [
            'code',
            'desc',
            'plantnumber',
            'serialnumber',
            'swl',
            'model',
            'dateofmanf',
            'manf',
            'subloc',
            'locnumber',
            'statuslive',
        ];
    }
}

if (!function_exists('plantChangeSetLastError')) {
    function plantChangeSetLastError(string $message): void
    {
        $GLOBALS['plant_change_last_error'] = $message;
    }
}

if (!function_exists('plantChangeLastError')) {
    function plantChangeLastError(): string
    {
        return (string)($GLOBALS['plant_change_last_error'] ?? '');
    }
}

if (!function_exists('plantChangeDebugLogPath')) {
    function plantChangeDebugLogPath(): string
    {
        $script = (string)($_SERVER['SCRIPT_FILENAME'] ?? '');
        $dir = $script !== '' ? dirname($script) : __DIR__;
        return rtrim($dir, "/\\") . '/plant_change_debug.log';
    }
}

if (!function_exists('plantChangeDebugLog')) {
    function plantChangeDebugLog(string $message): void
    {
        $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL;
        @error_log($line, 3, plantChangeDebugLogPath());
    }
}

if (!function_exists('plantChangeLogFailure')) {
    function plantChangeLogFailure(string $message): void
    {
        plantChangeSetLastError($message);
        plantChangeDebugLog('FAIL: ' . $message);
        error_log('[plant_changes] ' . $message);
    }
}

if (!function_exists('plantChangeDbReady')) {
    function plantChangeDbReady(string $table = 'plant_change_log'): bool
    {
        static $ready = [];
        if (array_key_exists($table, $ready)) {
            return $ready[$table];
        }

        if (empty($GLOBALS['con']) || !($GLOBALS['con'] instanceof mysqli)) {
            plantChangeLogFailure('Database connection is not available for ' . $table . '.');
            return false;
        }

        $safeTable = preg_replace('/[^A-Za-z0-9_]/', '', $table);
        if ($safeTable === '') {
            plantChangeLogFailure('Invalid plant change table requested.');
            return false;
        }

        $stmt = @$GLOBALS['con']->prepare("SELECT `id` FROM `{$safeTable}` LIMIT 1");
        if (!$stmt) {
            plantChangeLogFailure('Plant change table check failed for ' . $safeTable . ': ' . (string)($GLOBALS['con']->error ?? ''));
            return false;
        }

        $stmt->execute();
        $stmt->close();
        return $ready[$table] = true;
    }
}

if (!function_exists('plantChangeReportColumnReady')) {
    function plantChangeReportColumnReady(string $column): bool
    {
        static $ready = [];
        $safeColumn = preg_replace('/[^A-Za-z0-9_]/', '', $column);
        if ($safeColumn === '') {
            return false;
        }
        if (array_key_exists($safeColumn, $ready)) {
            return $ready[$safeColumn];
        }
        if (!plantChangeDbReady('plant_change_reports')) {
            return $ready[$safeColumn] = false;
        }

        $stmt = @$GLOBALS['con']->prepare("SELECT `{$safeColumn}` FROM `plant_change_reports` LIMIT 1");
        if (!$stmt) {
            return $ready[$safeColumn] = false;
        }
        $stmt->execute();
        $stmt->close();
        return $ready[$safeColumn] = true;
    }
}

if (!function_exists('plantChangeSnapshot')) {
    function plantChangeSnapshot(int $itemnumber): array
    {
        if ($itemnumber <= 0 || empty($GLOBALS['con']) || !($GLOBALS['con'] instanceof mysqli)) {
            plantChangeLogFailure('Unable to snapshot plant item; database connection or item number is invalid.');
            return [];
        }

        $fields = array_map(static function ($field): string {
            return 'i.`' . $field . '`';
        }, plantChangeTrackedFields());

        $sql = "
            SELECT i.`itemnumber`, i.`clientnumber`, " . implode(', ', $fields) . ",
                   d.`frequency` AS `plant_frequency`, d.`units` AS `plant_units`,
                   l.`locname`
            FROM `itemdetails` i
            LEFT JOIN `dd_plant` d ON d.`code` = i.`code`
            LEFT JOIN `locations` l ON l.`locnumber` = i.`locnumber`
            WHERE i.`itemnumber` = ?
            LIMIT 1
        ";

        $stmt = $GLOBALS['con']->prepare($sql);
        if (!$stmt) {
            plantChangeLogFailure('Unable to prepare plant snapshot lookup: ' . (string)($GLOBALS['con']->error ?? ''));
            return [];
        }

        $stmt->bind_param('i', $itemnumber);
        $stmt->execute();
        $res = $stmt->get_result();
        $stmt->close();
        $row = $res ? $res->fetch_assoc() : null;

        if (!is_array($row)) {
            plantChangeLogFailure('Plant snapshot was not found for item ' . $itemnumber . '.');
            return [];
        }

        return $row;
    }
}

if (!function_exists('plantChangeNormaliseValue')) {
    function plantChangeNormaliseValue($value): string
    {
        if ($value === null) {
            return '';
        }

        return trim((string)$value);
    }
}

if (!function_exists('plantChangeJson')) {
    function plantChangeJson(array $value): string
    {
        $json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return $json === false ? '{}' : $json;
    }
}

if (!function_exists('plantChangeLogRows')) {
    function plantChangeLogRows(string $eventType, array $oldSnapshot, array $newSnapshot, string $source, array $explicitFields = []): void
    {
        plantChangeDebugLog('START log rows event=' . $eventType . ' source=' . $source . ' script=' . (string)($_SERVER['SCRIPT_FILENAME'] ?? ''));
        if (!plantChangeDbReady('plant_change_log')) {
            return;
        }

        $eventType = substr(trim($eventType), 0, 40);
        $source = substr(trim($source), 0, 80);
        if ($eventType === '') {
            return;
        }

        $snapshot = !empty($newSnapshot) ? $newSnapshot : $oldSnapshot;
        $clientnumber = (int)($snapshot['clientnumber'] ?? 0);
        $itemnumber = (int)($snapshot['itemnumber'] ?? 0);
        $locnumber = (int)($snapshot['locnumber'] ?? 0);
        if ($clientnumber <= 0 || $itemnumber <= 0) {
            plantChangeLogFailure('Plant change log skipped because snapshot has no client/item number.');
            return;
        }

        $fields = $explicitFields;
        if (empty($fields)) {
            foreach (plantChangeTrackedFields() as $field) {
                if (plantChangeNormaliseValue($oldSnapshot[$field] ?? '') !== plantChangeNormaliseValue($newSnapshot[$field] ?? '')) {
                    $fields[] = $field;
                }
            }
        }

        if (empty($fields)) {
            plantChangeLogFailure('Plant change log skipped because no tracked fields changed.');
            return;
        }

        $oldJson = plantChangeJson($oldSnapshot);
        $newJson = plantChangeJson($newSnapshot);
        $oldFrequency = isset($oldSnapshot['plant_frequency']) ? (int)$oldSnapshot['plant_frequency'] : null;
        $newFrequency = isset($newSnapshot['plant_frequency']) ? (int)$newSnapshot['plant_frequency'] : null;
        $oldUnits = isset($oldSnapshot['plant_units']) ? (float)$oldSnapshot['plant_units'] : null;
        $newUnits = isset($newSnapshot['plant_units']) ? (float)$newSnapshot['plant_units'] : null;
        $actorId = (int)($_SESSION['userid'] ?? 0);
        $actorName = substr((string)($_SESSION['user'] ?? ''), 0, 150);
        $changedAt = time();
        $changeGroup = date('YmdHis') . '-' . $itemnumber . '-' . substr(sha1($eventType . microtime(true) . random_int(1, 999999)), 0, 8);

        $stmt = $GLOBALS['con']->prepare("
            INSERT INTO `plant_change_log`
            (`change_group`,`clientnumber`,`itemnumber`,`locnumber`,`event_type`,`field_name`,`old_value`,`new_value`,
             `old_snapshot_json`,`new_snapshot_json`,`old_frequency`,`new_frequency`,`old_units`,`new_units`,
             `changed_by`,`changed_by_name`,`changed_at`,`source`,`created_at`)
            VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        ");

        if (!$stmt) {
            plantChangeLogFailure('Unable to prepare plant change log insert: ' . (string)($GLOBALS['con']->error ?? ''));
            return;
        }

        foreach (array_values(array_unique($fields)) as $fieldName) {
            $fieldName = substr((string)$fieldName, 0, 60);
            $oldValue = plantChangeNormaliseValue($oldSnapshot[$fieldName] ?? '');
            $newValue = plantChangeNormaliseValue($newSnapshot[$fieldName] ?? '');

            $stmt->bind_param(
                'siiissssssiiddisisi',
                $changeGroup,
                $clientnumber,
                $itemnumber,
                $locnumber,
                $eventType,
                $fieldName,
                $oldValue,
                $newValue,
                $oldJson,
                $newJson,
                $oldFrequency,
                $newFrequency,
                $oldUnits,
                $newUnits,
                $actorId,
                $actorName,
                $changedAt,
                $source,
                $changedAt
            );
            if (!$stmt->execute()) {
                plantChangeLogFailure('Unable to insert plant change log row: ' . (string)($stmt->error ?? ''));
            } else {
                plantChangeDebugLog('OK: inserted plant change row event=' . $eventType . ' field=' . $fieldName . ' client=' . $clientnumber . ' item=' . $itemnumber . ' source=' . $source);
            }
        }

        $stmt->close();
    }
}

if (!function_exists('plantChangeLogAdd')) {
    function plantChangeLogAdd(int $itemnumber, string $source = 'asset_insert'): void
    {
        plantChangeDebugLog('CALL add item=' . $itemnumber . ' source=' . $source);
        plantChangeLogRows('added', [], plantChangeSnapshot($itemnumber), $source, ['statuslive']);
    }
}

if (!function_exists('plantChangeLogUpdate')) {
    function plantChangeLogUpdate(array $oldSnapshot, int $itemnumber, string $source = 'asset_update'): void
    {
        plantChangeDebugLog('CALL update item=' . $itemnumber . ' source=' . $source . ' old_snapshot=' . (empty($oldSnapshot) ? 'empty' : 'present'));
        $newSnapshot = plantChangeSnapshot($itemnumber);
        if (empty($oldSnapshot) || empty($newSnapshot)) {
            plantChangeLogFailure('Plant change update skipped because old or new snapshot is empty for item ' . $itemnumber . '.');
            return;
        }

        $oldStatus = (int)($oldSnapshot['statuslive'] ?? 0);
        $newStatus = (int)($newSnapshot['statuslive'] ?? 0);
        if ($oldStatus !== $newStatus) {
            plantChangeLogRows($newStatus === 1 ? 'reinstated' : 'archived', $oldSnapshot, $newSnapshot, $source, ['statuslive']);
        }

        $fields = [];
        foreach (plantChangeTrackedFields() as $field) {
            if ($field === 'statuslive') {
                continue;
            }
            if (plantChangeNormaliseValue($oldSnapshot[$field] ?? '') !== plantChangeNormaliseValue($newSnapshot[$field] ?? '')) {
                $fields[] = $field;
            }
        }

        if (!empty($fields)) {
            $event = in_array('code', $fields, true) || in_array('desc', $fields, true) ? 'code_changed' : 'updated';
            plantChangeLogRows($event, $oldSnapshot, $newSnapshot, $source, $fields);
        }
    }
}

if (!function_exists('plantChangeFetchLogs')) {
    function plantChangeFetchLogs(int $clientnumber, int $start, int $end): array
    {
        if ($clientnumber <= 0 || !plantChangeDbReady('plant_change_log')) {
            plantChangeDebugLog('FETCH skipped client=' . $clientnumber . ' start=' . $start . ' end=' . $end);
            return [];
        }

        if ($end <= 0) {
            $end = time();
        }

        plantChangeDebugLog(
            'FETCH start client=' . $clientnumber .
            ' start=' . $start . ' (' . date('Y-m-d H:i:s', $start) . ')' .
            ' end=' . $end . ' (' . date('Y-m-d H:i:s', $end) . ')'
        );

        $stmt = $GLOBALS['con']->prepare("
            SELECT pcl.*, l.`locname`
            FROM `plant_change_log` pcl
            LEFT JOIN `locations` l ON l.`locnumber` = pcl.`locnumber`
            WHERE pcl.`clientnumber` = ? AND pcl.`changed_at` BETWEEN ? AND ?
            ORDER BY pcl.`changed_at` ASC, pcl.`id` ASC
        ");
        if (!$stmt) {
            plantChangeLogFailure('Unable to prepare plant change log fetch: ' . (string)($GLOBALS['con']->error ?? ''));
            return [];
        }

        $stmt->bind_param('iii', $clientnumber, $start, $end);
        if (!$stmt->execute()) {
            plantChangeLogFailure('Unable to execute plant change log fetch: ' . (string)($stmt->error ?? ''));
            $stmt->close();
            return [];
        }
        $res = $stmt->get_result();
        $stmt->close();

        $rows = [];
        while ($res && ($row = $res->fetch_assoc())) {
            $rows[] = $row;
        }

        $events = [];
        foreach ($rows as $row) {
            $event = (string)($row['event_type'] ?? '');
            $events[$event] = ($events[$event] ?? 0) + 1;
        }
        plantChangeDebugLog('FETCH result client=' . $clientnumber . ' rows=' . count($rows) . ' events=' . json_encode($events));
        return $rows;
    }
}

if (!function_exists('plantChangeSummarise')) {
    function plantChangeSummarise(array $rows, array $manualEntries = []): array
    {
        $groups = [];
        foreach ($rows as $row) {
            $key = (string)($row['change_group'] ?? $row['id']);
            if (!isset($groups[$key])) {
                $groups[$key] = [
                    'rows' => [],
                    'first' => $row,
                ];
            }
            $groups[$key]['rows'][] = $row;
        }
        plantChangeDebugLog('SUMMARISE rows=' . count($rows) . ' groups=' . count($groups));

        $lines = [];
        $entries = [];
        $addedEntryIndexes = [];
        $uiDelta = 0.0;
        $addedUi = 0.0;
        $deletedUi = 0.0;
        foreach ($groups as $group) {
            $first = $group['first'];
            $old = json_decode((string)($first['old_snapshot_json'] ?? '{}'), true);
            $new = json_decode((string)($first['new_snapshot_json'] ?? '{}'), true);
            $old = is_array($old) ? $old : [];
            $new = is_array($new) ? $new : [];
            $snapshot = !empty($new) ? $new : $old;
            $event = (string)($first['event_type'] ?? 'updated');
            $desc = trim((string)($snapshot['desc'] ?? $old['desc'] ?? 'Item'));
            $serial = trim((string)($snapshot['serialnumber'] ?? $old['serialnumber'] ?? ''));
            $serialText = $serial !== '' ? ' s/n ' . $serial : '';
            $plant = trim((string)($snapshot['plantnumber'] ?? $old['plantnumber'] ?? ''));
            $plantText = $plant !== '' ? ' p/n ' . $plant : '';
            $loc = trim((string)($first['locname'] ?? $snapshot['locname'] ?? $old['locname'] ?? ''));
            $locText = $loc !== '' ? ' (' . $loc . ')' : '';
            $oldUnits = isset($first['old_units']) ? (float)$first['old_units'] : 0.0;
            $newUnits = isset($first['new_units']) ? (float)$first['new_units'] : 0.0;
            $baseEntry = [
                'group' => (string)($first['change_group'] ?? $first['id'] ?? ''),
                'itemnumber' => (int)($first['itemnumber'] ?? 0),
                'event_type' => $event,
                'desc' => $desc,
                'code' => trim((string)($snapshot['code'] ?? $old['code'] ?? '')),
                'serialnumber' => $serial,
                'plantnumber' => $plant,
                'locname' => $loc,
                'units' => $event === 'archived' ? $oldUnits : $newUnits,
                'duplicate' => false,
                'duplicate_reasons' => [],
            ];

            if ($event === 'added') {
                $line = 'Add - ' . $desc . $serialText . $plantText . $locText;
                $lines[] = $line;
                $entries[] = array_merge($baseEntry, ['type' => 'add', 'line' => $line]);
                $addedEntryIndexes[] = count($entries) - 1;
                $uiDelta += $newUnits;
                $addedUi += $newUnits;
                continue;
            }
            if ($event === 'archived') {
                $line = 'Delete - ' . $desc . $serialText . $plantText . $locText;
                $lines[] = $line;
                $entries[] = array_merge($baseEntry, ['type' => 'delete', 'line' => $line]);
                $uiDelta -= $oldUnits;
                $deletedUi += $oldUnits;
                continue;
            }
            if ($event === 'reinstated') {
                $line = 'Reinstate - ' . $desc . $serialText . $plantText . $locText;
                $lines[] = $line;
                $entries[] = array_merge($baseEntry, ['type' => 'add', 'line' => $line]);
                $addedEntryIndexes[] = count($entries) - 1;
                $uiDelta += $newUnits;
                $addedUi += $newUnits;
                continue;
            }

            $oldFreq = (int)($first['old_frequency'] ?? 0);
            $newFreq = (int)($first['new_frequency'] ?? 0);
            if ($event === 'code_changed' && $oldFreq > 0 && $newFreq > 0 && $oldFreq !== $newFreq) {
                $line = 'Change of frequency from ' . $oldFreq . ' monthly to ' . $newFreq . ' monthly ' . $desc . $serialText . $plantText . $locText;
                $lines[] = $line;
                $entries[] = array_merge($baseEntry, ['type' => 'alter', 'line' => $line]);
                $uiDelta += ($newUnits - $oldUnits);
                continue;
            }

            $changedFields = array_map(static function ($row): string {
                return (string)($row['field_name'] ?? '');
            }, $group['rows']);
            $changedFields = array_filter(array_unique($changedFields));
            $line = 'Alter - ' . $desc . $serialText . $plantText . $locText . ' (' . implode(', ', $changedFields) . ')';
            $lines[] = $line;
            $entries[] = array_merge($baseEntry, ['type' => 'alter', 'line' => $line]);
        }

        $normaliseDuplicateValue = static function ($value): string {
            return preg_replace('/\s+/', ' ', strtolower(trim((string)$value)));
        };
        foreach ($addedEntryIndexes as $leftOffset => $leftIndex) {
            for ($rightOffset = $leftOffset + 1; $rightOffset < count($addedEntryIndexes); $rightOffset++) {
                $rightIndex = $addedEntryIndexes[$rightOffset];
                $left = $entries[$leftIndex];
                $right = $entries[$rightIndex];
                $leftCode = $normaliseDuplicateValue($left['code'] ?? '');
                $rightCode = $normaliseDuplicateValue($right['code'] ?? '');
                $leftDesc = $normaliseDuplicateValue($left['desc'] ?? '');
                $rightDesc = $normaliseDuplicateValue($right['desc'] ?? '');
                $sameItemType = ($leftCode !== '' && $leftCode === $rightCode) || ($leftDesc !== '' && $leftDesc === $rightDesc);
                if (!$sameItemType) {
                    continue;
                }

                $reasons = [];
                $leftSerial = $normaliseDuplicateValue($left['serialnumber'] ?? '');
                $rightSerial = $normaliseDuplicateValue($right['serialnumber'] ?? '');
                if ($leftSerial !== '' && $leftSerial === $rightSerial) {
                    $reasons[] = 'serial number';
                }
                $leftPlant = $normaliseDuplicateValue($left['plantnumber'] ?? '');
                $rightPlant = $normaliseDuplicateValue($right['plantnumber'] ?? '');
                if ($leftPlant !== '' && $leftPlant === $rightPlant) {
                    $reasons[] = 'plant number';
                }
                if (empty($reasons)) {
                    continue;
                }

                foreach ([$leftIndex, $rightIndex] as $entryIndex) {
                    $entries[$entryIndex]['duplicate'] = true;
                    $entries[$entryIndex]['duplicate_reasons'] = array_values(array_unique(array_merge($entries[$entryIndex]['duplicate_reasons'], $reasons)));
                }
            }
        }

        foreach ($manualEntries as $entry) {
            if (!is_array($entry)) {
                continue;
            }

            $type = (string)($entry['type'] ?? '');
            $desc = trim((string)($entry['desc'] ?? 'Item'));
            $serial = trim((string)($entry['serialnumber'] ?? ''));
            $plant = trim((string)($entry['plantnumber'] ?? ''));
            $loc = trim((string)($entry['locname'] ?? ''));
            $date = trim((string)($entry['date'] ?? ''));
            $ui = isset($entry['ui']) ? (float)$entry['ui'] : 0.0;
            $serialText = $serial !== '' ? ' s/n ' . $serial : '';
            $plantText = $plant !== '' ? ' p/n ' . $plant : '';
            $locText = $loc !== '' ? ' (' . $loc . ')' : '';
            $dateText = $date !== '' ? ' on ' . $date : '';

            if ($type === 'manual_add') {
                $line = 'Add - ' . $desc . $serialText . $plantText . $locText . $dateText;
                $lines[] = $line;
                $entries[] = [
                    'type' => 'add',
                    'line' => $line,
                    'group' => '',
                    'manual' => true,
                    'duplicate' => false,
                    'duplicate_reasons' => [],
                ];
                $uiDelta += $ui;
                $addedUi += $ui;
                continue;
            }

            if ($type === 'manual_remove') {
                $line = 'Delete - ' . $desc . $serialText . $plantText . $locText . $dateText;
                $lines[] = $line;
                $entries[] = [
                    'type' => 'delete',
                    'line' => $line,
                    'group' => '',
                    'manual' => true,
                    'duplicate' => false,
                    'duplicate_reasons' => [],
                ];
                $uiDelta -= $ui;
                $deletedUi += $ui;
            }
        }

        $duplicateAdded = array_values(array_filter($entries, static function (array $entry): bool {
            return !empty($entry['duplicate']) && ($entry['type'] ?? '') === 'add' && empty($entry['manual']);
        }));

        return [
            'lines' => $lines,
            'entries' => $entries,
            'duplicate_added' => $duplicateAdded,
            'ui_delta' => round($uiDelta, 2),
            'added_ui' => round($addedUi, 2),
            'deleted_ui' => round($deletedUi, 2),
            'group_count' => count($groups),
        ];
    }
}

if (!function_exists('plantChangeMoneyValue')) {
    function plantChangeMoneyValue(string $value): ?float
    {
        $value = trim(str_replace([',', '£'], '', $value));
        return $value === '' || !is_numeric($value) ? null : round((float)$value, 2);
    }
}

if (!function_exists('plantChangeTenantSegment')) {
    function plantChangeTenantSegment(): string
    {
        $tenant = defined('TENANT_URL') ? (string)TENANT_URL : (string)($_REQUEST['tenant'] ?? '');
        $tenant = trim(str_replace('\\', '/', $tenant), '/');
        return preg_match('/\A[A-Za-z0-9][A-Za-z0-9._-]{0,190}\z/', $tenant) ? $tenant : '';
    }
}

if (!function_exists('plantChangeTenantFsPath')) {
    function plantChangeTenantFsPath(string $relative): string
    {
        $tenant = plantChangeTenantSegment();
        return '../../../' . $tenant . '/' . ltrim(str_replace('\\', '/', $relative), '/');
    }
}

if (!function_exists('plantChangeTenantWebPath')) {
    function plantChangeTenantWebPath(string $relative): string
    {
        return '/' . ltrim(str_replace('\\', '/', $relative), '/');
    }
}

if (!function_exists('plantChangeClientDetails')) {
    function plantChangeClientDetails(int $clientnumber): array
    {
        $stmt = $GLOBALS['con']->prepare("SELECT * FROM `clientdetails` WHERE `clientnumber`=? LIMIT 1");
        if (!$stmt) {
            return [];
        }
        $stmt->bind_param('i', $clientnumber);
        $stmt->execute();
        $res = $stmt->get_result();
        $stmt->close();
        $row = $res ? $res->fetch_assoc() : [];
        return is_array($row) ? $row : [];
    }
}

if (!function_exists('plantChangeCurrentAdminDetails')) {
    function plantChangeCurrentAdminDetails(): array
    {
        $adminId = (int)($_SESSION['admin_id'] ?? ($_SESSION['userid'] ?? 0));
        if ($adminId <= 0 || empty($GLOBALS['con']) || !($GLOBALS['con'] instanceof mysqli)) {
            return [];
        }

        $stmt = $GLOBALS['con']->prepare("SELECT `id`, `name`, `tel`, `email` FROM `admins` WHERE `id`=? LIMIT 1");
        if (!$stmt) {
            return [];
        }

        $stmt->bind_param('i', $adminId);
        $stmt->execute();
        $res = $stmt->get_result();
        $stmt->close();
        $row = $res ? $res->fetch_assoc() : [];
        return is_array($row) ? $row : [];
    }
}

if (!function_exists('plantChangeScheduleItems')) {
    function plantChangeScheduleItems(int $clientnumber): array
    {
        $stmt = $GLOBALS['con']->prepare("
            SELECT i.`itemnumber`, i.`desc`, i.`manf`, i.`model`, i.`serialnumber`, i.`plantnumber`,
                   i.`locnumber`, l.`locname`,
                   d.`frequency`, COALESCE(d.`units`, 0) AS `units`
            FROM `itemdetails` i
            LEFT JOIN `locations` l ON l.`locnumber` = i.`locnumber`
            LEFT JOIN `dd_plant` d ON d.`code` = i.`code`
            WHERE i.`clientnumber`=? AND i.`statuslive`='1'
            ORDER BY l.`locname`, i.`desc`, i.`serialnumber`, i.`plantnumber`
        ");
        if (!$stmt) {
            return [];
        }
        $stmt->bind_param('i', $clientnumber);
        $stmt->execute();
        $res = $stmt->get_result();
        $stmt->close();
        $rows = [];
        while ($res && ($row = $res->fetch_assoc())) {
            $rows[] = $row;
        }
        return $rows;
    }
}

if (!function_exists('plantChangeRenderTemplate')) {
    function plantChangeRenderTemplate(array $data): string
    {
        $template = plantChangeTenantFsPath('engines/plant/endorsement.php');
        if (!is_file($template)) {
            plantChangeSetLastError('Plant endorsement template is missing: engines/plant/endorsement.php');
            return '';
        }

        $plantChangePdf = $data;
        ob_start();
        include $template;
        return (string)ob_get_clean();
    }
}

if (!function_exists('plantChangeRenderAcceptanceTemplate')) {
    function plantChangeRenderAcceptanceTemplate(array $data): string
    {
        $template = plantChangeTenantFsPath('engines/plant/acceptance.php');
        if (!is_file($template)) {
            plantChangeSetLastError('Plant acceptance template is missing: engines/plant/acceptance.php');
            return '';
        }

        $plantChangePdf = $data;
        ob_start();
        include $template;
        return (string)ob_get_clean();
    }
}

if (!function_exists('plantChangePdfTemplatePart')) {
    function plantChangePdfTemplatePart(string $name, array $data): string
    {
        $path = plantChangeTenantFsPath('engines/plant/' . $name);
        if (!is_file($path)) {
            return '';
        }

        $html = (string)file_get_contents($path);
        $tenantBase = plantChangeTenantFsPath('');
        $clientnumber = (int)($data['client']['clientnumber'] ?? 0);
        $mainLogo = $tenantBase . 'images/company/logo.jpg';
        $clientLogo = $tenantBase . 'images/client/' . $clientnumber . '.jpg';
        $mainLogoHtml = is_file($mainLogo) ? '<img src="' . $mainLogo . '" height="40px" />' : '';
        $clientLogoHtml = is_file($clientLogo) ? '<td align="center"><img src="' . $clientLogo . '" height="40px" /></td>' : '';

        $repl = [
            '[LGO]' => $mainLogoHtml,
            '[LGOF]' => is_file($mainLogo) ? '<img src="' . $mainLogo . '" height="20px" />' : '',
            '[CL1]' => $clientLogoHtml,
            '[QR]' => '',
            '[STAT]' => '',
            '[CNAME]' => (string)($data['company']['name'] ?? ''),
            '[WEB]' => function_exists('getCompanyDetails') ? (string)getCompanyDetails('web') : '',
            '[ADDRESS]' => function_exists('getCompanyDetails') ? (string)getCompanyDetails('addressLong') : (string)($data['company']['address'] ?? ''),
            '[CONTACT]' => function_exists('getCompanyDetails') ? (string)getCompanyDetails('contact') : '',
            '[TEL]' => (string)($data['company']['tel'] ?? ''),
            '[EMAIL]' => (string)($data['company']['email'] ?? ''),
            '[REPORT_DATE]' => date('d-m-Y'),
            '[REPNO]' => (string)($data['report_id'] ?? ''),
        ];

        for ($i = 1; $i <= 6; $i++) {
            $img = $tenantBase . 'images/company/afl_' . $i . '.jpg';
            $repl['[AF' . $i . ']'] = is_file($img) ? '<td><img src="' . $img . '" height="40px" /></td>' : '';
        }

        return str_replace(array_keys($repl), array_values($repl), $html);
    }
}

if (!function_exists('plantChangeGeneratePdf')) {
    function plantChangeGeneratePdf(
        int $reportId,
        int $clientnumber,
        int $start,
        int $end,
        array $summary,
        string $manualLines,
        string $summaryJson,
        string $manualEntriesJson,
        float $baseContractValue,
        float $newContractValue,
        float $adminFeeValue,
        float $netAmount,
        float $vatRateValue,
        float $vatAmount,
        float $finalAmount
    ): string {
        $autoload = __DIR__ . '/../../packages/mpd/vendor/autoload.php';
        if (!is_file($autoload)) {
            plantChangeSetLastError('mPDF autoload file is missing.');
            return '';
        }
        require_once $autoload;

        $dir = plantChangeTenantFsPath('output/plant_change_reports/' . $clientnumber);
        if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
            plantChangeSetLastError('Unable to create plant change report output folder.');
            return '';
        }

        $filename = 'plant_change_' . $clientnumber . '_' . $reportId . '_' . date('Ymd_His') . '.pdf';
        $filePath = $dir . '/' . $filename;
        $webPath = plantChangeTenantWebPath('output/plant_change_reports/' . $clientnumber . '/' . $filename);

        $html = plantChangeRenderTemplate([
            'report_id' => $reportId,
            'client' => plantChangeClientDetails($clientnumber),
            'admin' => plantChangeCurrentAdminDetails(),
            'schedule_items' => plantChangeScheduleItems($clientnumber),
            'range_start' => $start,
            'range_end' => $end,
            'summary' => $summary,
            'summary_json' => $summaryJson,
            'manual_lines' => $manualLines,
            'manual_entries_json' => $manualEntriesJson,
            'base_contract_amount' => $baseContractValue,
            'new_contract_amount' => $newContractValue,
            'admin_fee' => $adminFeeValue,
            'net_amount' => $netAmount,
            'vat_rate' => $vatRateValue,
            'vat_amount' => $vatAmount,
            'final_amount' => $finalAmount,
            'created_at' => time(),
            'company' => [
                'name' => function_exists('getCompanyDetails') ? getCompanyDetails('name') : '',
                'name_short' => function_exists('getCompanyDetails') ? getCompanyDetails('name_short') : '',
                'tel' => function_exists('getCompanyDetails') ? getCompanyDetails('tel') : '',
                'email' => function_exists('getCompanyDetails') ? getCompanyDetails('email') : '',
                'address' => function_exists('getCompanyDetails') ? getCompanyDetails('address') : '',
            ],
        ]);
        if ($html === '') {
            return '';
        }

        $pdfData = [
            'report_id' => $reportId,
            'client' => plantChangeClientDetails($clientnumber),
            'company' => [
                'name' => function_exists('getCompanyDetails') ? getCompanyDetails('name') : '',
                'name_short' => function_exists('getCompanyDetails') ? getCompanyDetails('name_short') : '',
                'tel' => function_exists('getCompanyDetails') ? getCompanyDetails('tel') : '',
                'email' => function_exists('getCompanyDetails') ? getCompanyDetails('email') : '',
                'address' => function_exists('getCompanyDetails') ? getCompanyDetails('address') : '',
            ],
        ];
        $headerHtml = plantChangePdfTemplatePart('header.php', $pdfData);
        $footerHtml = plantChangePdfTemplatePart('footer.php', $pdfData);

        try {
            $mpdf = new \Mpdf\Mpdf([
                'mode' => 'utf-8',
                'format' => 'A4',
                'orientation' => 'P',
                'margin_top' => 10,
                'margin_header' => 5,
                'margin_footer' => 3,
                'margin_left' => 12,
                'margin_right' => 12,
                'margin_bottom' => 10,
                'setAutoTopMargin' => 'stretch',
                'setAutoBottomMargin' => 'stretch',
            ]);
            $mpdf->shrink_tables_to_fit = 1;
            if (trim($headerHtml) !== '') {
                $mpdf->SetHTMLHeader(mb_convert_encoding($headerHtml, 'UTF-8', 'UTF-8'), 'O');
                $mpdf->SetHTMLHeader(mb_convert_encoding($headerHtml, 'UTF-8', 'UTF-8'), 'E');
            }
            if (trim($footerHtml) !== '') {
                $mpdf->SetHTMLFooter(mb_convert_encoding($footerHtml, 'UTF-8', 'UTF-8'), 'O');
                $mpdf->SetHTMLFooter(mb_convert_encoding($footerHtml, 'UTF-8', 'UTF-8'), 'E');
            }
            $mpdf->WriteHTML(mb_convert_encoding($html, 'UTF-8', 'UTF-8'));
            $mpdf->Output($filePath, \Mpdf\Output\Destination::FILE);
        } catch (Throwable $e) {
            plantChangeSetLastError('Unable to generate plant change PDF: ' . $e->getMessage());
            return '';
        }

        return $webPath;
    }
}

if (!function_exists('plantChangeGenerateAcceptancePdf')) {
    function plantChangeGenerateAcceptancePdf(int $reportId, int $clientnumber, int $renewalDate, array $report): string
    {
        $autoload = __DIR__ . '/../../packages/mpd/vendor/autoload.php';
        if (!is_file($autoload)) {
            plantChangeSetLastError('mPDF autoload file is missing.');
            return '';
        }
        require_once $autoload;

        $dir = plantChangeTenantFsPath('output/plant_change_reports/' . $clientnumber);
        if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
            plantChangeSetLastError('Unable to create plant change report output folder.');
            return '';
        }

        $filename = 'new_cover_confirmed_' . $clientnumber . '_' . $reportId . '_' . date('Ymd_His') . '.pdf';
        $filePath = $dir . '/' . $filename;
        $webPath = plantChangeTenantWebPath('output/plant_change_reports/' . $clientnumber . '/' . $filename);
        $summary = json_decode((string)($report['summary_json'] ?? '{}'), true);
        $summary = is_array($summary) ? $summary : [];
        $serviceStart = $renewalDate > 0 ? $renewalDate : (int)($report['range_end'] ?? time());
        $serviceEnd = strtotime('-1 day', strtotime('+1 year', $serviceStart)) ?: (int)($report['range_end'] ?? $serviceStart);
        $client = plantChangeClientDetails($clientnumber);
        $company = [
            'name' => function_exists('getCompanyDetails') ? getCompanyDetails('name') : '',
            'name_short' => function_exists('getCompanyDetails') ? getCompanyDetails('name_short') : '',
            'tel' => function_exists('getCompanyDetails') ? getCompanyDetails('tel') : '',
            'email' => function_exists('getCompanyDetails') ? getCompanyDetails('email') : '',
            'address' => function_exists('getCompanyDetails') ? getCompanyDetails('address') : '',
        ];
        $pdfData = [
            'report_id' => $reportId,
            'client' => $client,
            'admin' => plantChangeCurrentAdminDetails(),
            'schedule_items' => plantChangeScheduleItems($clientnumber),
            'range_start' => $serviceStart,
            'range_end' => $serviceEnd,
            'summary' => $summary,
            'summary_json' => (string)($report['summary_json'] ?? '{}'),
            'manual_lines' => (string)($report['additional_lines'] ?? ''),
            'manual_entries_json' => (string)($report['manual_entries_json'] ?? '[]'),
            'base_contract_amount' => (float)($report['base_contract_amount'] ?? 0),
            'new_contract_amount' => (float)($report['new_contract_amount'] ?? 0),
            'admin_fee' => (float)($report['admin_fee'] ?? 0),
            'net_amount' => (float)($report['net_amount'] ?? 0),
            'vat_rate' => (float)($report['vat_rate'] ?? 0),
            'vat_amount' => (float)($report['vat_amount'] ?? 0),
            'final_amount' => (float)($report['final_amount'] ?? 0),
            'created_at' => time(),
            'company' => $company,
        ];

        $html = plantChangeRenderAcceptanceTemplate($pdfData);
        if ($html === '') {
            return '';
        }

        $headerHtml = plantChangePdfTemplatePart('header.php', $pdfData);
        $footerHtml = plantChangePdfTemplatePart('footer.php', $pdfData);

        try {
            $mpdf = new \Mpdf\Mpdf([
                'mode' => 'utf-8',
                'format' => 'A4',
                'orientation' => 'P',
                'margin_top' => 10,
                'margin_header' => 5,
                'margin_footer' => 3,
                'margin_left' => 12,
                'margin_right' => 12,
                'margin_bottom' => 10,
                'setAutoTopMargin' => 'stretch',
                'setAutoBottomMargin' => 'stretch',
            ]);
            $mpdf->shrink_tables_to_fit = 1;
            if (trim($headerHtml) !== '') {
                $mpdf->SetHTMLHeader(mb_convert_encoding($headerHtml, 'UTF-8', 'UTF-8'), 'O');
                $mpdf->SetHTMLHeader(mb_convert_encoding($headerHtml, 'UTF-8', 'UTF-8'), 'E');
            }
            if (trim($footerHtml) !== '') {
                $mpdf->SetHTMLFooter(mb_convert_encoding($footerHtml, 'UTF-8', 'UTF-8'), 'O');
                $mpdf->SetHTMLFooter(mb_convert_encoding($footerHtml, 'UTF-8', 'UTF-8'), 'E');
            }
            $mpdf->WriteHTML(mb_convert_encoding($html, 'UTF-8', 'UTF-8'));
            $mpdf->Output($filePath, \Mpdf\Output\Destination::FILE);
        } catch (Throwable $e) {
            plantChangeSetLastError('Unable to generate acceptance PDF: ' . $e->getMessage());
            return '';
        }

        return $webPath;
    }
}

if (!function_exists('plantChangeStoreAcceptancePdfPath')) {
    function plantChangeStoreAcceptancePdfPath(int $reportId, int $clientnumber, string $pdfPath): bool
    {
        $row = plantChangeReportRow($reportId, $clientnumber);
        if (!$row) {
            return false;
        }

        $summary = json_decode((string)($row['summary_json'] ?? '{}'), true);
        $summary = is_array($summary) ? $summary : [];
        $summary['acceptance_pdf_path'] = $pdfPath;
        $summary['acceptance_pdf_created_at'] = time();
        $summaryJson = plantChangeJson($summary);

        $stmt = $GLOBALS['con']->prepare("UPDATE `plant_change_reports` SET `summary_json`=? WHERE `id`=? AND `clientnumber`=? LIMIT 1");
        if (!$stmt) {
            plantChangeSetLastError('Unable to prepare acceptance PDF path update: ' . (string)($GLOBALS['con']->error ?? ''));
            return false;
        }
        $stmt->bind_param('sii', $summaryJson, $reportId, $clientnumber);
        $ok = $stmt->execute();
        if (!$ok) {
            plantChangeSetLastError('Unable to update acceptance PDF path: ' . (string)($stmt->error ?? ''));
        }
        $stmt->close();
        return $ok;
    }
}

if (!function_exists('plantChangeSaveReport')) {
    function plantChangeSaveReport(
        int $clientnumber,
        int $start,
        int $end,
        array $summary,
        string $manualLines,
        string $unitRate,
        string $baseContractAmount,
        string $newContractAmount,
        string $adminFee,
        string $vatRate,
        string $manualEntriesJson = '',
        string $endorsementType = 'full_term',
        string $midTermAmount = '',
        string $midTermAdminFee = '',
        string $upliftPercent = ''
    ): bool
    {
        if ($clientnumber <= 0 || !plantChangeDbReady('plant_change_reports')) {
            plantChangeSetLastError('Plant change report table is not available.');
            return false;
        }

        $endorsementType = $endorsementType === 'mid_term' ? 'mid_term' : 'full_term';
        $manualLines = trim($manualLines);
        $manualEntries = json_decode($manualEntriesJson, true);
        $manualEntriesJson = plantChangeJson(is_array($manualEntries) ? $manualEntries : []);
        $pdfPath = '';
        $actorId = (int)($_SESSION['userid'] ?? 0);
        $createdAt = time();
        $automaticUnits = (float)($summary['ui_delta'] ?? 0);
        $unitRateValue = plantChangeMoneyValue($unitRate) ?? 0.0;
        $baseContractValue = plantChangeMoneyValue($baseContractAmount) ?? 0.0;
        $newContractValue = plantChangeMoneyValue($newContractAmount) ?? $baseContractValue;
        $automaticAmount = round($newContractValue - $baseContractValue, 2);
        $manualLineAmountValue = 0.0;
        $adminFeeValue = plantChangeMoneyValue($adminFee) ?? 0.0;
        $manualOverrideValue = null;
        $vatRateValue = plantChangeMoneyValue($vatRate) ?? 0.0;
        $upliftPercentValue = max(0.0, plantChangeMoneyValue($upliftPercent) ?? 0.0);

        if ($endorsementType === 'mid_term') {
            $automaticAmount = round((((float)($summary['added_ui'] ?? 0.0)) - ((float)($summary['deleted_ui'] ?? 0.0))) * $unitRateValue, 2);
            $manualOverrideValue = plantChangeMoneyValue($midTermAmount);
            $newContractValue = $manualOverrideValue ?? $automaticAmount;
            $adminFeeValue = plantChangeMoneyValue($midTermAdminFee) ?? 0.0;
        }

        $preUpliftNetAmount = round($newContractValue + $adminFeeValue, 2);
        $upliftAmount = round($preUpliftNetAmount * ($upliftPercentValue / 100), 2);
        $netAmount = round($preUpliftNetAmount + $upliftAmount, 2);
        if ($endorsementType === 'full_term') {
            $newContractValue = round($netAmount - $adminFeeValue, 2);
        }
        $vatAmount = round($netAmount * ($vatRateValue / 100), 2);
        $finalAmount = round($netAmount + $vatAmount, 2);
        $summary['endorsement_type'] = $endorsementType;
        $summary['automatic_amount'] = $automaticAmount;
        $summary['manual_net_override'] = $manualOverrideValue;
        $summaryJson = plantChangeJson($summary);

        if (plantChangeDuplicateReportExists($clientnumber, $start, $end, $endorsementType)) {
            plantChangeSetLastError('A ' . plantChangeReportTypeLabel($endorsementType) . ' document already exists for this client and period.');
            return false;
        }

        $stmt = $GLOBALS['con']->prepare("
            INSERT INTO `plant_change_reports`
            (`clientnumber`,`range_start`,`range_end`,`automatic_units_delta`,`unit_rate`,`base_contract_amount`,
             `new_contract_amount`,`automatic_amount`,`manual_line_amount`,`admin_fee`,`manual_net_override`,
             `net_amount`,`vat_rate`,`vat_amount`,`final_amount`,
             `additional_lines`,`manual_entries_json`,`summary_json`,`pdf_path`,`created_by`,`created_at`)
            VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        ");
        if (!$stmt) {
            plantChangeSetLastError('Unable to prepare report history insert: ' . (string)($GLOBALS['con']->error ?? ''));
            return false;
        }

        $stmt->bind_param(
            'iiiddddddddddddssssii',
            $clientnumber,
            $start,
            $end,
            $automaticUnits,
            $unitRateValue,
            $baseContractValue,
            $newContractValue,
            $automaticAmount,
            $manualLineAmountValue,
            $adminFeeValue,
            $manualOverrideValue,
            $netAmount,
            $vatRateValue,
            $vatAmount,
            $finalAmount,
            $manualLines,
            $manualEntriesJson,
            $summaryJson,
            $pdfPath,
            $actorId,
            $createdAt
        );
        $ok = $stmt->execute();
        if (!$ok) {
            plantChangeSetLastError('Unable to save report history: ' . (string)($stmt->error ?? ''));
        }
        $reportId = (int)($ok ? $GLOBALS['con']->insert_id : 0);
        $stmt->close();

        if ($ok && $reportId > 0) {
            $generatedPdfPath = plantChangeGeneratePdf(
                $reportId,
                $clientnumber,
                $start,
                $end,
                $summary,
                $manualLines,
                $summaryJson,
                $manualEntriesJson,
                $baseContractValue,
                $newContractValue,
                $adminFeeValue,
                $netAmount,
                $vatRateValue,
                $vatAmount,
                $finalAmount
            );
            if ($generatedPdfPath === '') {
                return false;
            }

            $stmtPdf = $GLOBALS['con']->prepare("UPDATE `plant_change_reports` SET `pdf_path`=? WHERE `id`=? LIMIT 1");
            if (!$stmtPdf) {
                plantChangeSetLastError('Unable to prepare PDF path update: ' . (string)($GLOBALS['con']->error ?? ''));
                return false;
            }
            $stmtPdf->bind_param('si', $generatedPdfPath, $reportId);
            $pdfOk = $stmtPdf->execute();
            if (!$pdfOk) {
                plantChangeSetLastError('Unable to update PDF path: ' . (string)($stmtPdf->error ?? ''));
            }
            $stmtPdf->close();
            return $pdfOk;
        }

        return $ok;
    }
}

if (!function_exists('plantChangeReportRow')) {
    function plantChangeReportRow(int $reportId, int $clientnumber = 0): ?array
    {
        if ($reportId <= 0 || !plantChangeDbReady('plant_change_reports')) {
            plantChangeSetLastError('Plant change report table is not available.');
            return null;
        }

        if ($clientnumber > 0) {
            $stmt = $GLOBALS['con']->prepare("SELECT * FROM `plant_change_reports` WHERE `id`=? AND `clientnumber`=? LIMIT 1");
            if (!$stmt) {
                plantChangeSetLastError('Unable to prepare report lookup: ' . (string)($GLOBALS['con']->error ?? ''));
                return null;
            }
            $stmt->bind_param('ii', $reportId, $clientnumber);
        } else {
            $stmt = $GLOBALS['con']->prepare("SELECT * FROM `plant_change_reports` WHERE `id`=? LIMIT 1");
            if (!$stmt) {
                plantChangeSetLastError('Unable to prepare report lookup: ' . (string)($GLOBALS['con']->error ?? ''));
                return null;
            }
            $stmt->bind_param('i', $reportId);
        }

        $stmt->execute();
        $res = $stmt->get_result();
        $row = $res ? $res->fetch_assoc() : null;
        $stmt->close();

        if (!$row) {
            plantChangeSetLastError('Plant change report was not found.');
            return null;
        }

        return $row;
    }
}

if (!function_exists('plantChangeRemoveReport')) {
    function plantChangeRemoveReport(int $reportId, int $clientnumber): bool
    {
        $row = plantChangeReportRow($reportId, $clientnumber);
        if (!$row) {
            return false;
        }
        if (!empty($row['accepted_at'])) {
            plantChangeSetLastError('Accepted plant change reports are historic and cannot be removed.');
            return false;
        }

        $removedAt = time();
        $removedBy = (int)($_SESSION['userid'] ?? 0);
        $stmt = $GLOBALS['con']->prepare("UPDATE `plant_change_reports` SET `removed_at`=?, `removed_by`=? WHERE `id`=? AND `clientnumber`=? LIMIT 1");
        if (!$stmt) {
            plantChangeSetLastError('Unable to prepare report removal: ' . (string)($GLOBALS['con']->error ?? ''));
            return false;
        }
        $stmt->bind_param('iiii', $removedAt, $removedBy, $reportId, $clientnumber);
        $ok = $stmt->execute();
        if (!$ok) {
            plantChangeSetLastError('Unable to remove report: ' . (string)($stmt->error ?? ''));
        }
        $stmt->close();
        if (!$ok) {
            return false;
        }

        if (!plantChangeDeleteReportPdf($row)) {
            return false;
        }

        $stmtClear = $GLOBALS['con']->prepare("UPDATE `plant_change_reports` SET `pdf_path`='' WHERE `id`=? AND `clientnumber`=? LIMIT 1");
        if ($stmtClear) {
            $stmtClear->bind_param('ii', $reportId, $clientnumber);
            $stmtClear->execute();
            $stmtClear->close();
        }

        return true;
    }
}

if (!function_exists('plantChangeRemoveLogGroup')) {
    function plantChangeRemoveLogGroup(int $clientnumber, string $changeGroup): bool
    {
        $changeGroup = trim($changeGroup);
        if ($clientnumber <= 0 || $changeGroup === '' || !plantChangeDbReady('plant_change_log')) {
            plantChangeSetLastError('Unable to identify the plant change log entry to remove.');
            return false;
        }

        $stmt = $GLOBALS['con']->prepare("DELETE FROM `plant_change_log` WHERE `clientnumber`=? AND `change_group`=? LIMIT 20");
        if (!$stmt) {
            plantChangeSetLastError('Unable to prepare plant change log removal: ' . (string)($GLOBALS['con']->error ?? ''));
            return false;
        }

        $stmt->bind_param('is', $clientnumber, $changeGroup);
        $ok = $stmt->execute();
        if (!$ok) {
            plantChangeSetLastError('Unable to remove plant change log entry: ' . (string)($stmt->error ?? ''));
        } elseif ($stmt->affected_rows <= 0) {
            plantChangeSetLastError('The selected plant change log entry was not found.');
            $ok = false;
        }
        $stmt->close();
        return $ok;
    }
}

if (!function_exists('plantChangeMarkReportSent')) {
    function plantChangeMarkReportSent(int $reportId, int $clientnumber, string $emailTo): bool
    {
        if (!plantChangeReportRow($reportId, $clientnumber)) {
            return false;
        }

        $sentAt = time();
        $sentBy = (int)($_SESSION['userid'] ?? 0);
        $emailTo = trim($emailTo);
        $stmt = $GLOBALS['con']->prepare("UPDATE `plant_change_reports` SET `email_sent_at`=?, `email_sent_by`=?, `email_to`=? WHERE `id`=? AND `clientnumber`=? LIMIT 1");
        if (!$stmt) {
            plantChangeSetLastError('Unable to prepare email tracking update: ' . (string)($GLOBALS['con']->error ?? ''));
            return false;
        }
        $stmt->bind_param('iisii', $sentAt, $sentBy, $emailTo, $reportId, $clientnumber);
        $ok = $stmt->execute();
        if (!$ok) {
            plantChangeSetLastError('Unable to track email send: ' . (string)($stmt->error ?? ''));
        }
        $stmt->close();
        return $ok;
    }
}

if (!function_exists('plantChangeReportEndorsementType')) {
    function plantChangeReportEndorsementType(array $row): string
    {
        $summary = json_decode((string)($row['summary_json'] ?? '{}'), true);
        $type = is_array($summary) ? (string)($summary['endorsement_type'] ?? '') : '';
        return $type === 'mid_term' ? 'mid_term' : 'full_term';
    }
}

if (!function_exists('plantChangeDeleteReportPdf')) {
    function plantChangeDeleteReportPdf(array $row): bool
    {
        $pdfPaths = [];
        $mainPdfPath = trim((string)($row['pdf_path'] ?? ''));
        if ($mainPdfPath !== '') {
            $pdfPaths[] = $mainPdfPath;
        }

        $summary = json_decode((string)($row['summary_json'] ?? '{}'), true);
        if (is_array($summary)) {
            foreach (['acceptance_pdf_path'] as $pathKey) {
                $extraPath = trim((string)($summary[$pathKey] ?? ''));
                if ($extraPath !== '') {
                    $pdfPaths[] = $extraPath;
                }
            }
        }

        foreach (array_unique($pdfPaths) as $pdfPath) {
            $relative = ltrim(parse_url($pdfPath, PHP_URL_PATH) ?: $pdfPath, '/');
            if ($relative === '' || strpos($relative, '..') !== false || !preg_match('#\A[A-Za-z0-9._/\- ]+\z#', $relative)) {
                plantChangeSetLastError('Unable to remove saved PDF because the stored path is invalid.');
                return false;
            }

            $filePath = plantChangeTenantFsPath($relative);
            if (!is_file($filePath)) {
                continue;
            }

            if (!@unlink($filePath)) {
                plantChangeSetLastError('Unable to delete generated plant change PDF.');
                return false;
            }
        }

        return true;
    }
}

if (!function_exists('plantChangeReportTypeLabel')) {
    function plantChangeReportTypeLabel(string $type): string
    {
        return $type === 'mid_term' ? 'Mid Term' : 'Renewal';
    }
}

if (!function_exists('plantChangeDuplicateReportExists')) {
    function plantChangeDuplicateReportExists(int $clientnumber, int $start, int $end, string $endorsementType): bool
    {
        if ($clientnumber <= 0 || !plantChangeDbReady('plant_change_reports')) {
            return false;
        }

        $stmt = $GLOBALS['con']->prepare("
            SELECT `summary_json`
            FROM `plant_change_reports`
            WHERE `clientnumber`=? AND `range_start`=? AND `range_end`=?
              AND (`removed_at` IS NULL OR `removed_at`=0 OR `removed_at`='')
        ");
        if (!$stmt) {
            return false;
        }

        $stmt->bind_param('iii', $clientnumber, $start, $end);
        $stmt->execute();
        $res = $stmt->get_result();
        $stmt->close();

        while ($res && ($row = $res->fetch_assoc())) {
            if (plantChangeReportEndorsementType($row) === $endorsementType) {
                return true;
            }
        }

        return false;
    }
}

if (!function_exists('plantChangeAcceptReport')) {
    function plantChangeAcceptReport(int $reportId, int $clientnumber, int $renewalDate = 0): bool
    {
        $row = plantChangeReportRow($reportId, $clientnumber);
        if (!$row) {
            return false;
        }
        if (!empty($row['removed_at'])) {
            plantChangeSetLastError('Removed plant change reports cannot be accepted.');
            return false;
        }
        if (!empty($row['accepted_at'])) {
            plantChangeSetLastError('This plant change report has already been accepted.');
            return false;
        }

        if (plantChangeReportEndorsementType($row) === 'mid_term') {
            $acceptedAt = time();
            $acceptedBy = (int)($_SESSION['userid'] ?? 0);
            $stmtReport = $GLOBALS['con']->prepare("UPDATE `plant_change_reports` SET `accepted_at`=?, `accepted_by`=? WHERE `id`=? AND `clientnumber`=? LIMIT 1");
            if (!$stmtReport) {
                plantChangeSetLastError('Unable to prepare mid term acceptance update: ' . (string)($GLOBALS['con']->error ?? ''));
                return false;
            }
            $stmtReport->bind_param('iiii', $acceptedAt, $acceptedBy, $reportId, $clientnumber);
            $ok = $stmtReport->execute();
            if (!$ok) {
                plantChangeSetLastError('Unable to mark mid term report accepted: ' . (string)($stmtReport->error ?? ''));
            }
            $stmtReport->close();
            return $ok;
        }

        $newRate = round((float)($row['new_contract_amount'] ?? 0), 2);
        if ($newRate <= 0) {
            plantChangeSetLastError('The saved report does not contain a valid new contract value.');
            return false;
        }

        $GLOBALS['con']->begin_transaction();
        try {
            $rateText = number_format($newRate, 2, '.', '');
            $stmtClient = $GLOBALS['con']->prepare("UPDATE `clientdetails` SET `rate_per_hour`=?, `renewal_date`=?, `renewal_status`='ACTIVE' WHERE `clientnumber`=? LIMIT 1");
            if (!$stmtClient) {
                throw new RuntimeException('Unable to prepare client renewal update: ' . (string)($GLOBALS['con']->error ?? ''));
            }
            $stmtClient->bind_param('sii', $rateText, $renewalDate, $clientnumber);
            if (!$stmtClient->execute()) {
                throw new RuntimeException('Unable to update client renewal value: ' . (string)($stmtClient->error ?? ''));
            }
            $stmtClient->close();

            $acceptedAt = time();
            $acceptedBy = (int)($_SESSION['userid'] ?? 0);
            $stmtReport = $GLOBALS['con']->prepare("UPDATE `plant_change_reports` SET `accepted_at`=?, `accepted_by`=?, `accepted_rate_per_hour`=? WHERE `id`=? AND `clientnumber`=? LIMIT 1");
            if (!$stmtReport) {
                throw new RuntimeException('Unable to prepare report acceptance update: ' . (string)($GLOBALS['con']->error ?? ''));
            }
            $stmtReport->bind_param('iidii', $acceptedAt, $acceptedBy, $newRate, $reportId, $clientnumber);
            if (!$stmtReport->execute()) {
                throw new RuntimeException('Unable to mark report accepted: ' . (string)($stmtReport->error ?? ''));
            }
            $stmtReport->close();

            $GLOBALS['con']->commit();
            return true;
        } catch (Throwable $e) {
            $GLOBALS['con']->rollback();
            plantChangeSetLastError($e->getMessage());
            return false;
        }
    }
}

if (!function_exists('plantChangeRejectReport')) {
    function plantChangeRejectReport(int $reportId, int $clientnumber): bool
    {
        $row = plantChangeReportRow($reportId, $clientnumber);
        if (!$row) {
            return false;
        }
        if (plantChangeReportEndorsementType($row) !== 'full_term') {
            plantChangeSetLastError('Only full term endorsements can archive a client as rejected.');
            return false;
        }
        if (!empty($row['removed_at'])) {
            plantChangeSetLastError('Removed plant change reports cannot be rejected.');
            return false;
        }
        if (!empty($row['accepted_at'])) {
            plantChangeSetLastError('Accepted plant change reports cannot be rejected.');
            return false;
        }
        if (!plantChangeReportColumnReady('rejected_at') || !plantChangeReportColumnReady('rejected_by')) {
            plantChangeSetLastError('Plant change report rejection fields have not been applied to the database yet.');
            return false;
        }

        $GLOBALS['con']->begin_transaction();
        try {
            $stmtClient = $GLOBALS['con']->prepare("UPDATE `clientdetails` SET `statuslive`=0, `renewal_status`='LAPSED' WHERE `clientnumber`=? LIMIT 1");
            if (!$stmtClient) {
                throw new RuntimeException('Unable to prepare client archive update: ' . (string)($GLOBALS['con']->error ?? ''));
            }
            $stmtClient->bind_param('i', $clientnumber);
            if (!$stmtClient->execute()) {
                throw new RuntimeException('Unable to archive rejected client: ' . (string)($stmtClient->error ?? ''));
            }
            $stmtClient->close();

            $rejectedAt = time();
            $rejectedBy = (int)($_SESSION['userid'] ?? 0);
            $stmtReport = $GLOBALS['con']->prepare("UPDATE `plant_change_reports` SET `rejected_at`=?, `rejected_by`=? WHERE `id`=? AND `clientnumber`=? LIMIT 1");
            if (!$stmtReport) {
                throw new RuntimeException('Unable to prepare report rejection update: ' . (string)($GLOBALS['con']->error ?? ''));
            }
            $stmtReport->bind_param('iiii', $rejectedAt, $rejectedBy, $reportId, $clientnumber);
            if (!$stmtReport->execute()) {
                throw new RuntimeException('Unable to mark report rejected: ' . (string)($stmtReport->error ?? ''));
            }
            $stmtReport->close();

            $GLOBALS['con']->commit();
            return true;
        } catch (Throwable $e) {
            $GLOBALS['con']->rollback();
            plantChangeSetLastError($e->getMessage());
            return false;
        }
    }
}
