<?php
defined('ABSPATH') or die('Direct access not allowed');

/**
 * VollstartRD_Repairs — Repair CRUD + status management.
 *
 * SRP: Creates, reads, updates, deletes repairs.
 * Generates repair numbers and access codes.
 * Handles status transitions and logs them.
 */
class VollstartRD_Repairs {

    private $MAIN;

    /**
     * Default status definitions.
     * Each status has: label, color (hex), icon (dashicon slug), is_final flag.
     */
    /**
     * Default status definitions.
     * Labels use __() for i18n — base language is English.
     * Slugs are internal identifiers and never change.
     */
    private ?array $defaultStatuses = null;

    private function getDefaultStatusDefinitions(): array {
        if ($this->defaultStatuses === null) {
            $this->defaultStatuses = [
                'received'            => ['label' => __('Received',              'vollstart-repair-desk'), 'color' => '#3b82f6', 'icon' => 'download',    'is_final' => false],
                'diagnosis'           => ['label' => __('Diagnosis',             'vollstart-repair-desk'), 'color' => '#6366f1', 'icon' => 'visibility',  'is_final' => false],
                'quote'               => ['label' => __('Quote',                 'vollstart-repair-desk'), 'color' => '#f59e0b', 'icon' => 'media-text',  'is_final' => false],
                'waiting_for_parts'   => ['label' => __('Waiting for Parts',     'vollstart-repair-desk'), 'color' => '#f97316', 'icon' => 'clock',       'is_final' => false],
                'in_progress'         => ['label' => __('In Progress',           'vollstart-repair-desk'), 'color' => '#eab308', 'icon' => 'admin-tools', 'is_final' => false],
                'quality_check'       => ['label' => __('Quality Check',         'vollstart-repair-desk'), 'color' => '#8b5cf6', 'icon' => 'yes-alt',     'is_final' => false],
                'done'                => ['label' => __('Done',                  'vollstart-repair-desk'), 'color' => '#22c55e', 'icon' => 'yes',         'is_final' => false],
                'delivered'           => ['label' => __('Handed Out',            'vollstart-repair-desk'), 'color' => '#64748b', 'icon' => 'migrate',     'is_final' => true],
                'cancelled'           => ['label' => __('Cancelled',             'vollstart-repair-desk'), 'color' => '#ef4444', 'icon' => 'dismiss',     'is_final' => true],
            ];

            // A registered extension can add/override status definitions via
            // this filter. Each entry must be an array with
            // 'label', 'color', 'icon', 'is_final' and may set 'custom'.
            $this->defaultStatuses = apply_filters('vollstart_rd_status_definitions', $this->defaultStatuses);
        }
        return $this->defaultStatuses;
    }

    public function __construct($MAIN) {
        $this->MAIN = $MAIN;
    }

    /**
     * Create a new repair.
     * @return int Repair ID
     */
    public function create(array $data): int {
        $data = apply_filters('vollstart_rd_pre_create_repair', $data);

        $assignedTo = !empty($data['assigned_to']) ? (int) $data['assigned_to'] : null;
        $staffId = (int) ($data['staff_id'] ?? 0);

        // Auto-fill assigned_to from created_by_staff_id if not explicitly set
        if (empty($assignedTo) && !empty($staffId)) {
            $assignedTo = $staffId;
        }

        $fields = [
            'customer_id'        => (int) ($data['customer_id'] ?? 0),
            'product_id'         => !empty($data['product_id']) ? (int) $data['product_id'] : null,
            'repair_type_id'     => !empty($data['repair_type_id']) ? (int) $data['repair_type_id'] : null,
            'assigned_to'        => $assignedTo,
            'device_type'        => sanitize_text_field($data['device_type'] ?? ''),
            'device_brand'       => sanitize_text_field($data['device_brand'] ?? ''),
            'device_model'       => sanitize_text_field($data['device_model'] ?? ''),
            'device_serial'      => sanitize_text_field($data['device_serial'] ?? ''),
            'defect_description' => sanitize_textarea_field($data['defect_description'] ?? ''),
            'status'             => 'received',
            'priority'           => in_array($data['priority'] ?? 'normal', ['normal', 'high', 'urgent']) ? ($data['priority'] ?? 'normal') : 'normal',
            'estimated_date'     => !empty($data['estimated_date']) ? sanitize_text_field($data['estimated_date']) : null,
            'estimated_price'    => !empty($data['estimated_price']) ? (float) $data['estimated_price'] : null,
            'warranty_until'     => !empty($data['warranty_until']) ? sanitize_text_field($data['warranty_until']) : null,
            'internal_notes'     => sanitize_textarea_field($data['internal_notes'] ?? ''),
            'created_by_user_id' => get_current_user_id(),
            'created_by_staff_id' => $staffId,
            'access_code'        => $this->generateAccessCode(),
            'created_at'         => current_time('mysql'),
            'updated_at'         => current_time('mysql'),
        ];

        $id = $this->MAIN->getDB()->insert('repairs', $fields);

        // Generate repair number (F9)
        $number = $this->generateRepairNumber($id);
        $this->MAIN->getDB()->update('repairs', ['repair_number' => $number], ['id' => $id]);

        // Log initial status
        $this->logStatus($id, '', 'received', wp_get_current_user()->display_name ?: 'System', '');

        // Suppress maintenance warnings for linked device during repair
        if (!empty($fields['product_id'])) {
            $product = $this->MAIN->getProducts()->getById((int) $fields['product_id']);
            if ($product && !empty($product['service_interval_days'])) {
                $this->MAIN->getProducts()->dismissReminder((int) $fields['product_id']);
            }
        }

        do_action('vollstart_rd_post_create_repair', $id, $fields, $data);

        return $id;
    }

    /**
     * Update a repair.
     */
    public function update(int $id, array $data): bool {
        $data = apply_filters('vollstart_rd_pre_update_repair', $data, $id);
        $old = $this->getById($id);

        $fields = [];
        $allowed = ['customer_id', 'product_id', 'repair_type_id', 'assigned_to', 'device_type', 'device_brand', 'device_model', 'device_serial', 'defect_description', 'priority', 'estimated_date', 'estimated_price', 'warranty_until', 'recurring_interval_days', 'internal_notes'];

        foreach ($allowed as $key) {
            if (!isset($data[$key])) continue;

            switch ($key) {
                case 'customer_id':
                case 'product_id':
                case 'repair_type_id':
                case 'assigned_to':
                case 'recurring_interval_days':
                    $fields[$key] = !empty($data[$key]) ? (int) $data[$key] : null;
                    break;
                case 'estimated_price':
                    $fields[$key] = !empty($data[$key]) ? (float) $data[$key] : null;
                    break;
                case 'defect_description':
                case 'internal_notes':
                    $fields[$key] = sanitize_textarea_field($data[$key]);
                    break;
                case 'priority':
                    $fields[$key] = in_array($data[$key], ['normal', 'high', 'urgent']) ? $data[$key] : 'normal';
                    break;
                default:
                    $fields[$key] = sanitize_text_field($data[$key]);
            }
        }

        if (empty($fields)) {
            return false;
        }

        $fields['updated_at'] = current_time('mysql');
        $result = $this->MAIN->getDB()->update('repairs', $fields, ['id' => $id]) !== false;

        // Log every changed field to audit trail
        if ($result && $old) {
            $eventMap = [
                'customer_id' => 'customer_changed',
                'product_id' => 'product_linked',
                'internal_notes' => 'note_updated',
            ];
            $numericKeys = ['customer_id', 'product_id', 'repair_type_id', 'assigned_to', 'recurring_interval_days', 'estimated_price'];
            foreach ($fields as $key => $newVal) {
                if ($key === 'updated_at') continue;
                $oldVal = $old[$key] ?? '';
                // Normalize numeric values to avoid false diffs (100.00 vs 100)
                if (in_array($key, $numericKeys)) {
                    $oldNorm = $oldVal === null || $oldVal === '' ? '' : (strpos((string) $oldVal, '.') !== false ? rtrim(rtrim((string) $oldVal, '0'), '.') : (string) $oldVal);
                    $newNorm = $newVal === null || $newVal === '' ? '' : (strpos((string) $newVal, '.') !== false ? rtrim(rtrim((string) $newVal, '0'), '.') : (string) $newVal);
                } else {
                    $oldNorm = (string) $oldVal;
                    $newNorm = (string) ($newVal ?? '');
                }
                if ($oldNorm !== $newNorm) {
                    $eventType = $eventMap[$key] ?? 'repair_updated';
                    $this->logEvent($id, $eventType, '', $key, $oldNorm, $newNorm);
                }
            }

            // If repair_type_id changed, replace checklist items
            if (isset($fields['repair_type_id'])) {
                $oldTypeId = (int) ($old['repair_type_id'] ?? 0);
                $newTypeId = (int) ($fields['repair_type_id'] ?? 0);
                if ($oldTypeId !== $newTypeId) {
                    // Remove old checklist items
                    $this->MAIN->getDB()->delete('repair_checklist_items', ['repair_id' => $id]);
                    // Apply new ones if type is set
                    if ($newTypeId > 0) {
                        $this->MAIN->getChecklists()->applyToRepair($id, $newTypeId);
                    }
                }
            }
        }

        do_action('vollstart_rd_post_update_repair', $id, $fields, $data);

        return $result;
    }

    /**
     * Change repair status. Logs the transition.
     */
    public function changeStatus(int $id, string $newStatus, string $changedBy = '', string $note = ''): bool {
        $repair = $this->getById($id);
        if (!$repair) {
            return false;
        }

        $oldStatus = $repair['status'];
        if ($oldStatus === $newStatus) {
            return true;
        }

        $newStatus = apply_filters('vollstart_rd_pre_change_status', $newStatus, $id, $oldStatus);

        $result = $this->MAIN->getDB()->update('repairs', [
            'status'     => $newStatus,
            'updated_at' => current_time('mysql'),
        ], ['id' => $id]);

        if ($result !== false) {
            if (empty($changedBy)) {
                $changedBy = wp_get_current_user()->display_name ?: 'System';
            }
            $this->logStatus($id, $oldStatus, $newStatus, $changedBy, $note);

            // Send status change email to customer
            $this->MAIN->getNotifications()->sendStatusChangeEmail($id, $newStatus);

            // Schedule next maintenance when repair completes (done/delivered)
            if (in_array($newStatus, ['done', 'delivered']) && !empty($repair['product_id'])) {
                $this->MAIN->getProducts()->scheduleNextService((int) $repair['product_id']);
            }

            do_action('vollstart_rd_post_change_status', $id, $oldStatus, $newStatus, $note);
        }

        return $result !== false;
    }

    /**
     * Log a status change.
     */
    private function logStatus(int $repairId, string $oldStatus, string $newStatus, string $changedBy, string $note): void {
        $this->logEvent($repairId, 'status_change', $changedBy, $note, $oldStatus, $newStatus);
    }

    /**
     * Log any event on a repair (audit trail).
     * Event types: status_change, file_uploaded, file_deleted, note_updated,
     * customer_assigned, customer_changed, product_linked, repair_updated, repair_created
     */
    public function logEvent(int $repairId, string $eventType, string $changedBy = '', string $note = '', string $oldValue = '', string $newValue = ''): void {
        if (empty($changedBy)) {
            $changedBy = wp_get_current_user()->display_name ?: 'System';
        }
        $this->MAIN->getDB()->insert('status_log', [
            'repair_id'  => $repairId,
            'event_type' => $eventType,
            'old_status' => $oldValue,
            'new_status' => $newValue,
            'changed_by' => $changedBy,
            'note'       => $note,
            'created_at' => current_time('mysql'),
        ]);
    }

    /**
     * Get full audit log for a repair (status changes + all events).
     */
    public function getAuditLog(int $repairId): array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('status_log');
        return $db->getResults("SELECT * FROM %i WHERE repair_id = %d ORDER BY created_at ASC", $tbl, $repairId);
    }

    /**
     * Get status log for a repair.
     */
    public function getStatusLog(int $repairId): array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('status_log');
        return $db->getResults("SELECT * FROM %i WHERE repair_id = %d AND event_type IN ('status_change', 'time_tracked') ORDER BY created_at ASC", $tbl, $repairId);
    }

    /**
     * Get a single repair by ID.
     */
    public function getById(int $id): ?array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('repairs');
        return $db->getRow("SELECT * FROM %i WHERE id = %d", $tbl, $id);
    }

    /**
     * Get a repair by access code (for public status page).
     */
    public function getByAccessCode(string $code): ?array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('repairs');
        return $db->getRow("SELECT * FROM %i WHERE access_code = %s", $tbl, $code);
    }

    /**
     * Get all repairs with optional filters.
     */
    public function getAll(array $filters = []): array {
        $db = $this->MAIN->getDB();
        $repairs_tbl = $db->getTabelle('repairs');
        $customers_tbl = $db->getTabelle('customers');

        $where = [];
        $values = [$repairs_tbl, $customers_tbl];

        if (!empty($filters['status'])) {
            $where[] = "r.status = %s";
            $values[] = $filters['status'];
        }
        if (!empty($filters['priority'])) {
            $where[] = "r.priority = %s";
            $values[] = $filters['priority'];
        }
        if (!empty($filters['customer_id'])) {
            $where[] = "r.customer_id = %d";
            $values[] = (int) $filters['customer_id'];
        }
        if (!empty($filters['assigned_to'])) {
            $where[] = "r.assigned_to = %d";
            $values[] = (int) $filters['assigned_to'];
        }
        if (!empty($filters['product_id'])) {
            $where[] = "r.product_id = %d";
            $values[] = (int) $filters['product_id'];
        }
        if (!empty($filters['repair_type_id'])) {
            $where[] = "r.repair_type_id = %d";
            $values[] = (int) $filters['repair_type_id'];
        }
        if (!empty($filters['search'])) {
            $like = '%' . $db->escLike($filters['search']) . '%';
            $where[] = "(r.repair_number LIKE %s OR r.device_type LIKE %s OR r.device_brand LIKE %s OR r.device_model LIKE %s OR r.defect_description LIKE %s OR c.name LIKE %s)";
            $values = array_merge($values, [$like, $like, $like, $like, $like, $like]);
        }

        $whereStr = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';

        $sql = "SELECT r.*, c.name AS customer_name, c.customer_number, c.email AS customer_email, c.phone AS customer_phone
                FROM %i r
                LEFT JOIN %i c ON r.customer_id = c.id
                {$whereStr}
                ORDER BY FIELD(r.status, 'cancelled', 'delivered') ASC, r.priority DESC, r.created_at DESC";

        return $db->getResults($sql, ...$values);
    }

    /**
     * Server-side paginated list for DataTables.
     */
    public function getPaginated(array $params): array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('repairs');
        $ctbl = $db->getTabelle('customers');

        $page = max(1, (int) ($params['page'] ?? 1));
        $perPage = max(1, min(100, (int) ($params['per_page'] ?? 20)));
        $offset = ($page - 1) * $perPage;
        $search = $params['search'] ?? '';
        $status = $params['status'] ?? '';
        $orderBy = $params['orderby'] ?? 'created_at';
        $order = strtoupper($params['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';

        $sortable = ['repair_number', 'customer_name', 'device_brand', 'status', 'priority', 'created_at'];
        if (!in_array($orderBy, $sortable)) {
            $orderBy = 'created_at';
        }
        if ($orderBy === 'customer_name') $orderBy = 'c.name';
        elseif ($orderBy !== 'c.name') $orderBy = 'r.' . $orderBy;

        global $wpdb;

        $where = [];
        $whereValues = [];
        if (!empty($status)) {
            $where[] = "r.status = %s";
            $whereValues[] = $status;
        }
        if (!empty($search)) {
            $like = '%' . $db->escLike($search) . '%';
            $where[] = "(r.repair_number LIKE %s OR r.device_type LIKE %s OR r.device_brand LIKE %s OR r.device_model LIKE %s OR r.defect_description LIKE %s OR c.name LIKE %s)";
            array_push($whereValues, $like, $like, $like, $like, $like, $like);
        }

        $whereStr = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';

        $total = (int) $db->getVar("SELECT COUNT(*) FROM %i", $tbl);

        $filteredSql = "SELECT COUNT(*) FROM %i r LEFT JOIN %i c ON r.customer_id = c.id {$whereStr}";
        $filtered = (int) $db->getVar($filteredSql, ...array_merge([$tbl, $ctbl], $whereValues));

        $msgTbl = $db->getTabelle('messages');
        global $wpdb;
        $sql = "SELECT r.*, c.name AS customer_name, c.customer_number, c.email AS customer_email, c.phone AS customer_phone,
                u.display_name AS created_by_user_name,
                (SELECT COUNT(*) FROM {$msgTbl} m WHERE m.repair_id = r.id AND m.sender_type = 'customer' AND m.read_at IS NULL) AS unread_messages
                FROM %i r
                LEFT JOIN %i c ON r.customer_id = c.id
                LEFT JOIN {$wpdb->users} u ON r.created_by_user_id = u.ID
                {$whereStr}
                ORDER BY FIELD(r.status, 'cancelled', 'delivered') ASC, {$orderBy} {$order}
                LIMIT %d, %d";

        $values = array_merge([$tbl, $ctbl], $whereValues, [$offset, $perPage]);
        $rows = $db->getResults($sql, ...$values);

        return [
            'repairs'  => $rows,
            'total'    => $total,
            'filtered' => $filtered,
            'counts'   => $this->countByStatus(),
        ];
    }

    /**
     * Count repairs by status.
     */
    public function countByStatus(): array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('repairs');
        $rows = $db->getResults("SELECT status, COUNT(*) as cnt FROM %i GROUP BY status", $tbl);
        $counts = [];
        foreach ($rows as $row) {
            $counts[$row['status']] = (int) $row['cnt'];
        }
        return $counts;
    }

    /**
     * Delete a repair and all related data.
     */
    public function delete(int $id): bool {
        $db = $this->MAIN->getDB();
        // Delete related records
        $db->delete('status_log', ['repair_id' => $id]);
        $db->delete('messages', ['repair_id' => $id]);
        $db->delete('repair_checklist_items', ['repair_id' => $id]);
        // Delete files via file upload manager (handles physical files too)
        $this->MAIN->getFileUpload()->deleteByEntity('repair', $id);
        // Delete the repair itself
        return $db->delete('repairs', ['id' => $id]) !== false;
    }

    /**
     * Get default status definitions.
     */
    public function getDefaultStatuses(): array {
        return $this->getDefaultStatusDefinitions();
    }

    /**
     * Get active statuses in configured order.
     * Returns full status objects with label, color, icon, is_final.
     * Uses wp_cache to avoid repeated DB reads in same request.
     */
    public function getActiveStatuses(): array {
        $cached = wp_cache_get('vollstart_rd_active_statuses', 'vollstart-repair-desk');
        if ($cached !== false) {
            return $cached;
        }

        $flowJson = $this->MAIN->getOptions()->getOptionValue('statusFlow');
        $flow = json_decode($flowJson, true);

        if (!is_array($flow)) {
            wp_cache_set('vollstart_rd_active_statuses', $this->getDefaultStatusDefinitions(), 'vollstart-repair-desk');
            return $this->getDefaultStatusDefinitions();
        }

        $allDefs = $this->getDefaultStatusDefinitions();
        $result = [];
        foreach ($flow as $item) {
            $slug = $item['slug'] ?? '';
            if (!empty($item['active']) && isset($allDefs[$slug])) {
                $result[$slug] = $allDefs[$slug];
            }
        }
        // Include custom statuses not yet in flow (newly created)
        foreach ($allDefs as $slug => $def) {
            if (!empty($def['custom']) && !isset($result[$slug])) {
                $result[$slug] = $def;
            }
        }

        wp_cache_set('vollstart_rd_active_statuses', $result, 'vollstart-repair-desk');
        return $result;
    }

    /**
     * Get all statuses (including inactive and custom, for settings UI).
     */
    public function getAllStatuses(): array {
        return $this->getDefaultStatusDefinitions();
    }

    /**
     * Get the configured status flow order (with active flags).
     */
    public function getStatusFlowConfig(): array {
        $flowJson = $this->MAIN->getOptions()->getOptionValue('statusFlow');
        $flow = json_decode($flowJson, true);
        $allDefs = $this->getDefaultStatusDefinitions();

        if (!is_array($flow)) {
            $flow = [];
            foreach ($allDefs as $slug => $def) {
                $flow[] = ['slug' => $slug, 'active' => true];
            }
        }

        // Enrich with full status data
        $result = [];
        $flowSlugs = [];
        foreach ($flow as $item) {
            $slug = $item['slug'] ?? '';
            $flowSlugs[] = $slug;
            if (isset($allDefs[$slug])) {
                $result[] = array_merge($allDefs[$slug], [
                    'slug'   => $slug,
                    'active' => !empty($item['active']),
                ]);
            }
        }
        // Append custom statuses not yet in flow
        foreach ($allDefs as $slug => $def) {
            if (!empty($def['custom']) && !in_array($slug, $flowSlugs)) {
                $result[] = array_merge($def, ['slug' => $slug, 'active' => true]);
            }
        }

        return $result;
    }

    /**
     * Get human-readable label for a status slug.
     */
    public function getStatusLabel(string $slug): string {
        $defs = $this->getDefaultStatusDefinitions();
        return isset($defs[$slug]) ? $defs[$slug]['label'] : $slug;
    }

    /**
     * Get color for a status slug.
     */
    public function getStatusColor(string $slug): string {
        $defs = $this->getDefaultStatusDefinitions();
        return isset($defs[$slug]) ? $defs[$slug]['color'] : '#94a3b8';
    }

    /**
     * Check if a status is final (no further actions possible).
     */
    public function isFinalStatus(string $slug): bool {
        $defs = $this->getDefaultStatusDefinitions();
        return isset($defs[$slug]) && !empty($defs[$slug]['is_final']);
    }

    /**
     * Get repairs with warranty expiring within X days.
     */
    public function getExpiringWarranties(int $days): array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('repairs');
        $ctbl = $db->getTabelle('customers');
        $cutoff = gmdate('Y-m-d', strtotime('+' . $days . ' days'));
        return $db->getResults(
            "SELECT r.*, c.name AS customer_name, c.email AS customer_email
             FROM %i r
             INNER JOIN %i c ON r.customer_id = c.id
             WHERE r.warranty_until IS NOT NULL
               AND r.warranty_until <= %s
               AND r.warranty_until >= CURDATE()
               AND c.email != ''",
            $tbl, $ctbl, $cutoff
        );
    }

    /**
     * Get unread message count for a repair (messages from customer, not yet read by staff).
     */
    public function getUnreadMessageCount(int $repairId): int {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('messages');
        return (int) $db->getVar(
            "SELECT COUNT(*) FROM %i WHERE repair_id = %d AND sender_type = 'customer' AND read_at IS NULL",
            $tbl,
            $repairId
        );
    }

    /**
     * Mark all customer messages as read for a repair.
     */
    public function markMessagesRead(int $repairId): void {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('messages');
        $db->getVar("UPDATE %i SET read_at = %s WHERE repair_id = %d AND sender_type = 'customer' AND read_at IS NULL", $tbl, current_time('mysql'), $repairId);
    }

    /**
     * Generate repair number based on configured format.
     *
     * Formats:
     * - number_only:       REP-0001 (sequential, never resets)
     * - year_number:       REP-26-0001 (resets per year)
     * - yearmonth_number:  REP-2603-0001 (resets per month)
     */
    private function generateRepairNumber(int $repairId): string {
        $opts = $this->MAIN->getOptions();
        $prefix = $opts->getOptionValue('repairNumberPrefix') ?: 'REP-';
        $format = $opts->getOptionValue('repairNumberFormat') ?: 'number_only';
        $digits = max(3, min(6, (int) ($opts->getOptionValue('repairNumberDigits') ?: 4)));

        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('repairs');

        switch ($format) {
            case 'year_number':
                $yearPrefix = wp_date('y');
                $datePart = $yearPrefix . '-';
                // Count repairs this year for sequential number
                $yearStart = wp_date('Y') . '-01-01 00:00:00';
                $count = (int) $db->getVar(
                    "SELECT COUNT(*) FROM %i WHERE created_at >= %s",
                    $tbl,
                    $yearStart
                );
                $seq = str_pad($count, $digits, '0', STR_PAD_LEFT);
                return $prefix . $datePart . $seq;

            case 'yearmonth_number':
                $ymPrefix = wp_date('ym');
                $datePart = $ymPrefix . '-';
                // Count repairs this month for sequential number
                $monthStart = wp_date('Y-m') . '-01 00:00:00';
                $count = (int) $db->getVar(
                    "SELECT COUNT(*) FROM %i WHERE created_at >= %s",
                    $tbl,
                    $monthStart
                );
                $seq = str_pad($count, $digits, '0', STR_PAD_LEFT);
                return $prefix . $datePart . $seq;

            default: // number_only
                $seq = str_pad($repairId, $digits, '0', STR_PAD_LEFT);
                return $prefix . $seq;
        }
    }

    /**
     * Generate a unique access code for public status page.
     */
    private function generateAccessCode(): string {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('repairs');

        do {
            $code = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8));
            $code = substr($code, 0, 4) . '-' . substr($code, 4, 4);
            $existing = $db->getVar("SELECT id FROM %i WHERE access_code = %s", $tbl, $code);
        } while ($existing !== null);

        return $code;
    }

    /**
     * Regenerate access code for a repair. Old code stops working immediately.
     */
    public function regenerateAccessCode(int $id): ?string {
        $repair = $this->getById($id);
        if (!$repair) return null;
        $newCode = $this->generateAccessCode();
        $this->MAIN->getDB()->update('repairs', ['access_code' => $newCode], ['id' => $id]);
        $this->logEvent($id, 'access_code_changed', '', 'Access code regenerated', $repair['access_code'], $newCode);
        return $newCode;
    }
}
