<?php

declare(strict_types=1);

/**
 * Shared chair-occupancy helpers for queue seating and dentist treatment start.
 * Advisory locks are installation-scoped and keep concurrent requests from
 * seating or treating two patients in the same physical chair.
 */

function queue_chair_identity(array $queue): array
{
    return [
        'branch_id' => (int)($queue['branch_id'] ?? 0),
        'chair_id' => (int)($queue['chair_id'] ?? 0),
        'chair_name' => trim((string)($queue['chair_name'] ?? '')),
    ];
}

function queue_chair_lock_name(array $queue): string
{
    $identity = queue_chair_identity($queue);
    $chairKey = $identity['chair_id'] > 0
        ? 'id:' . $identity['chair_id']
        : 'name:' . mb_strtolower($identity['chair_name']);
    return 'dc_chair_' . substr(hash('sha256', table('queue_entries') . '|' . $identity['branch_id'] . '|' . $chairKey), 0, 44);
}

function queue_acquire_chair_lock(PDO $pdo, array $queue, int $timeoutSeconds = 5): string
{
    $identity = queue_chair_identity($queue);
    if ($identity['branch_id'] < 1) {
        throw new RuntimeException('The queue entry has no valid branch assignment.');
    }
    if ($identity['chair_id'] < 1 && $identity['chair_name'] === '') {
        throw new RuntimeException('Assign a treatment chair before seating or starting this patient.');
    }

    $name = queue_chair_lock_name($queue);
    $stmt = $pdo->prepare('SELECT GET_LOCK(?, ?)');
    $stmt->execute([$name, max(0, $timeoutSeconds)]);
    if ((int)$stmt->fetchColumn() !== 1) {
        throw new RuntimeException('The treatment chair is being updated by another user. Please try again.');
    }
    return $name;
}

function queue_release_chair_lock(PDO $pdo, ?string $lockName): void
{
    if (!$lockName) return;
    try {
        $stmt = $pdo->prepare('SELECT RELEASE_LOCK(?)');
        $stmt->execute([$lockName]);
    } catch (Throwable $exception) {
        error_log('DentalCare chair lock release failed: ' . $exception->getMessage());
    }
}

function queue_active_chair_occupants(PDO $pdo, array $queue, array $statuses = ['seated','in_treatment'], int $excludeQueueId = 0): array
{
    $identity = queue_chair_identity($queue);
    if ($identity['chair_id'] < 1 && $identity['chair_name'] === '') return [];

    $validStatuses = array_values(array_intersect($statuses, ['waiting','called','seated','in_treatment','waiting_billing','cashier_called']));
    if (!$validStatuses) return [];

    $statusSql = implode(',', array_fill(0, count($validStatuses), '?'));
    $args = [$identity['branch_id'], $excludeQueueId, ...$validStatuses];
    $chairSql = '';
    if ($identity['chair_id'] > 0) {
        $chairSql = '(q.chair_id=?';
        $args[] = $identity['chair_id'];
        if ($identity['chair_name'] !== '') {
            $chairSql .= ' OR (q.chair_id IS NULL AND LOWER(TRIM(COALESCE(q.chair_name,"")))=LOWER(TRIM(?)))';
            $args[] = $identity['chair_name'];
        }
        $chairSql .= ')';
    } else {
        $chairSql = 'LOWER(TRIM(COALESCE(q.chair_name,"")))=LOWER(TRIM(?))';
        $args[] = $identity['chair_name'];
    }

    $stmt = $pdo->prepare(
        'SELECT q.id,q.queue_no,q.status,q.patient_id,q.appointment_id,q.chair_id,q.chair_name,'
        . 'p.first_name,p.middle_name,p.last_name '
        . 'FROM `' . table('queue_entries') . '` q '
        . 'JOIN `' . table('patients') . '` p ON p.id=q.patient_id '
        . 'WHERE q.branch_id=? AND q.id<>? '
        . 'AND q.status IN (' . $statusSql . ') AND ' . $chairSql . ' '
        . 'ORDER BY FIELD(q.status,"in_treatment","seated","called","waiting"),q.checked_in_at'
    );
    $stmt->execute($args);
    return $stmt->fetchAll();
}

function queue_assert_chair_available_for_seating(PDO $pdo, array $queue): void
{
    $occupants = queue_active_chair_occupants($pdo, $queue, ['seated','in_treatment'], (int)($queue['id'] ?? 0));
    if (!$occupants) return;
    $other = $occupants[0];
    $name = trim(implode(' ', array_filter([(string)($other['first_name'] ?? ''), (string)($other['middle_name'] ?? ''), (string)($other['last_name'] ?? '')])));
    throw new RuntimeException(
        'Chair ' . (($queue['chair_name'] ?? '') ?: '#' . (int)($queue['chair_id'] ?? 0))
        . ' is already occupied by ' . ($name !== '' ? $name : (string)$other['queue_no'])
        . ' (' . (string)$other['queue_no'] . ', ' . ucwords(str_replace('_', ' ', (string)$other['status'])) . '). '
        . 'Return that patient to waiting or complete treatment before seating another patient.'
    );
}

/**
 * When legacy data already has multiple seated patients in one chair, the
 * dentist's explicit Start Treatment action selects the real occupant. Other
 * merely seated records are returned to waiting; an active treatment is never
 * moved automatically.
 */
function queue_resolve_duplicate_seating_for_start(PDO $pdo, array $queue, int $changedBy): array
{
    $active = queue_active_chair_occupants($pdo, $queue, ['in_treatment'], (int)($queue['id'] ?? 0));
    if ($active) {
        $other = $active[0];
        throw new RuntimeException('Chair ' . (($queue['chair_name'] ?? '') ?: '#' . (int)($queue['chair_id'] ?? 0)) . ' is still in treatment for ' . (string)$other['queue_no'] . '. Finalize or move that active patient first.');
    }

    $duplicates = queue_active_chair_occupants($pdo, $queue, ['seated'], (int)($queue['id'] ?? 0));
    $released = [];
    foreach ($duplicates as $other) {
        $otherId = (int)$other['id'];
        $releaseStmt = $pdo->prepare('UPDATE `' . table('queue_entries') . '` SET status="waiting",seated_at=NULL,updated_at=NOW() WHERE id=? AND status="seated"');
        $releaseStmt->execute([$otherId]);
        if ($releaseStmt->rowCount() > 0) {
            $note = 'Chair released automatically when treatment started for ' . (string)($queue['queue_no'] ?? ('queue #' . (int)($queue['id'] ?? 0)));
            $pdo->prepare(
                'INSERT INTO `' . table('queue_status_history') . '` '
                . '(queue_entry_id,previous_status,new_status,notes,changed_by,created_at) VALUES (?,"seated","waiting",?,?,NOW())'
            )->execute([$otherId, $note, $changedBy ?: null]);
            if (!empty($other['appointment_id'])) {
                $appointmentId = (int)$other['appointment_id'];
                $oldStatusStmt = $pdo->prepare('SELECT status FROM `' . table('appointments') . '` WHERE id=? FOR UPDATE');
                $oldStatusStmt->execute([$appointmentId]);
                $oldStatus = (string)($oldStatusStmt->fetchColumn() ?: 'checked_in');
                if (!in_array($oldStatus,['checked_in','called','seated'],true)) {
                    throw new RuntimeException('Duplicate chair recovery cannot downgrade appointment #' . $appointmentId . ' from ' . str_replace('_',' ',$oldStatus) . '.');
                }
                if ($oldStatus !== 'checked_in') {
                    $update=$pdo->prepare('UPDATE `' . table('appointments') . '` SET status="checked_in",updated_at=NOW() WHERE id=? AND status=?');
                    $update->execute([$appointmentId,$oldStatus]);
                    if($update->rowCount()!==1)throw new RuntimeException('The duplicate chair appointment changed while recovery was being completed.');
                }
                if ($oldStatus !== 'checked_in' && schema_table_exists(table('appointment_status_history'))) {
                    $pdo->prepare(
                        'INSERT INTO `' . table('appointment_status_history') . '` '
                        . '(appointment_id,previous_status,new_status,reason,changed_by,created_at) VALUES (?,?,"checked_in",?,?,NOW())'
                    )->execute([$appointmentId, $oldStatus, $note, $changedBy ?: null]);
                }
            }
            if (function_exists('workflow_event')) {
                workflow_event((int)$other['patient_id'],'queue_waiting','Returned to waiting after duplicate chair seating was resolved',['appointment_id'=>(int)($other['appointment_id']??0),'queue_entry_id'=>$otherId],$note);
            }
            if (function_exists('audit')) {
                audit('duplicate_chair_seating_resolved','queue_entry',$otherId,['status'=>'waiting','selected_queue_id'=>(int)($queue['id']??0),'chair_id'=>(int)($queue['chair_id']??0),'chair_name'=>(string)($queue['chair_name']??'')],['status'=>'seated']);
            }
            $released[] = (string)$other['queue_no'];
        }
    }
    return $released;
}

/**
 * Strict queue state machine. Forward care transitions are one-way; only
 * explicitly documented exception paths may move a patient backwards.
 */
function queue_allowed_transitions(string $providerType, string $status): array
{
    $providerType = $providerType === 'inhouse' ? 'inhouse' : 'dentist';
    $map = $providerType === 'inhouse'
        ? [
            'waiting' => ['called','cancelled'],
            'called' => ['waiting','seated','cancelled'],
            'seated' => ['waiting','cancelled'],
            'in_treatment' => [],
            'waiting_billing' => ['cashier_called'],
            'cashier_called' => [],
            'completed' => [],
            'cancelled' => [],
        ]
        : [
            'waiting' => ['called','cancelled'],
            'called' => ['waiting','seated','cancelled'],
            'seated' => ['waiting','cancelled'],
            'in_treatment' => [],
            'waiting_billing' => ['cashier_called'],
            'cashier_called' => [],
            'completed' => [],
            'cancelled' => [],
        ];
    return $map[$status] ?? [];
}

function queue_transition_requires_reason(string $from, string $to): bool
{
    return $to === 'cancelled'
        || ($from === 'called' && $to === 'waiting')
        || ($from === 'seated' && $to === 'waiting');
}

function queue_transition_action_label(string $from, string $to): string
{
    return match ($to) {
        'called' => 'Call patient',
        'seated' => 'Seat patient',
        'waiting' => $from === 'seated' ? 'Release chair' : 'Return to waiting',
        'waiting_billing' => 'Send to cashier',
        'cashier_called' => 'Call at cashier',
        'completed' => 'Complete visit',
        'cancelled' => in_array($from, ['waiting','called','seated'], true) ? 'Cancel / left' : 'Cancel visit',
        default => ucwords(str_replace('_', ' ', $to)),
    };
}

/**
 * Closes every appointment service segment when an appointment is cancelled,
 * marked no-show, or closed from the queue as left before treatment. This keeps
 * week/month schedule views and conflict checks from showing cancelled work as
 * still occupying a provider or chair.
 */
function queue_sync_terminal_appointment_segments(PDO $pdo, int $appointmentId, string $appointmentStatus): void
{
    if ($appointmentId < 1 || !in_array($appointmentStatus, ['cancelled','no_show','left_before_treatment'], true)) {
        return;
    }
    if (!function_exists('schema_table_exists') || !schema_table_exists(table('appointment_services'))) {
        return;
    }
    if (function_exists('schema_column_exists') && !schema_column_exists(table('appointment_services'), 'status')) {
        return;
    }
    $segmentStatus = $appointmentStatus === 'no_show' ? 'no_show' : 'cancelled';
    $stmt = $pdo->prepare(
        'UPDATE `' . table('appointment_services') . '` '
        . 'SET status=?,updated_at=NOW() '
        . 'WHERE appointment_id=? AND COALESCE(status,"") NOT IN ("completed","cancelled","no_show")'
    );
    $stmt->execute([$segmentStatus, $appointmentId]);
}
