<?php

declare(strict_types=1);

if (!defined('DENTAL_ROOT')) {
    exit;
}

/**
 * Appointment/schedule safety guard for v0.5.9.8.44.
 * Keeps scheduling conflicts, clinic-decides provider assignment, and status
 * transitions from becoming silent runtime or workflow errors.
 */

function appointment_schedule_guard_schema_ready(PDO $pdo): bool
{
    return schema_table_exists(table('appointment_assignment_queue'))
        && schema_table_exists(table('appointment_safety_events'))
        && schema_column_exists(table('appointments'), 'assignment_status')
        && schema_column_exists(table('appointments'), 'schedule_guard_status')
        && schema_column_exists(table('appointments'), 'schedule_guard_checked_at')
        && schema_column_exists(table('appointment_requests'), 'assignment_queue_status');
}

function appointment_guard_log_event(PDO $pdo, array $event): void
{
    if (!schema_table_exists(table('appointment_safety_events'))) return;
    $stmt = $pdo->prepare(
        'INSERT INTO `' . table('appointment_safety_events') . '` '
        . '(branch_id,patient_id,appointment_id,appointment_request_id,event_type,event_label,severity,message,metadata_json,created_by,created_at) '
        . 'VALUES (?,?,?,?,?,?,?,?,?,?,NOW())'
    );
    $metadata = $event['metadata'] ?? null;
    $stmt->execute([
        (int)($event['branch_id'] ?? 0) ?: null,
        (int)($event['patient_id'] ?? 0) ?: null,
        (int)($event['appointment_id'] ?? 0) ?: null,
        (int)($event['appointment_request_id'] ?? 0) ?: null,
        (string)($event['event_type'] ?? 'schedule_guard'),
        mb_substr((string)($event['event_label'] ?? 'Appointment safety event'), 0, 190),
        in_array((string)($event['severity'] ?? 'info'), ['info','warning','critical'], true) ? (string)($event['severity'] ?? 'info') : 'info',
        mb_substr((string)($event['message'] ?? ''), 0, 1000),
        $metadata === null ? null : json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
        (int)(current_user()['id'] ?? 0) ?: null,
    ]);
}

function appointment_guard_conflict_details(PDO $pdo, int $branchId, int $patientId, int $dentistUserId, int $chairId, int $inhouseProviderUserId, string $chairName, string $startAt, string $endAt, int $excludeAppointmentId = 0): array
{
    $appointmentTable = table('appointments');
    $details = [];
    $argsBase = [$branchId, $endAt, $startAt, $excludeAppointmentId];

    $checks = [
        ['patient', $patientId > 0, 'a.patient_id=?', [$patientId], 'Patient already has an overlapping active appointment.'],
        ['dentist', $dentistUserId > 0, 'a.dentist_user_id=?', [$dentistUserId], 'Dentist already has an overlapping active appointment.'],
        ['inhouse_provider', $inhouseProviderUserId > 0, 'a.inhouse_provider_user_id=?', [$inhouseProviderUserId], 'In-house provider already has an overlapping active appointment.'],
        ['chair', $chairId > 0 && schema_column_exists($appointmentTable, 'chair_id'), 'a.chair_id=?', [$chairId], 'Chair already has an overlapping active appointment.'],
        ['chair_name', $chairId < 1 && trim($chairName) !== '', 'LOWER(TRIM(COALESCE(a.chair_name,"")))=LOWER(TRIM(?))', [trim($chairName)], 'Chair already has an overlapping active appointment.'],
    ];

    foreach ($checks as [$type, $enabled, $condition, $extraArgs, $message]) {
        if (!$enabled) continue;
        $sql = 'SELECT a.id,a.status,a.start_at,a.end_at,a.service_name,a.patient_id,a.dentist_name,a.inhouse_provider_name,a.chair_name '
            . 'FROM `' . $appointmentTable . '` a '
            . 'WHERE a.branch_id=? AND a.status NOT IN ("cancelled","no_show","completed") '
            . 'AND a.start_at<? AND a.end_at>? AND a.id<>? AND ' . $condition . ' '
            . 'ORDER BY a.start_at ASC LIMIT 5';
        $stmt = $pdo->prepare($sql);
        $stmt->execute(array_merge($argsBase, $extraArgs));
        $rows = $stmt->fetchAll();
        if ($rows) {
            $details[] = ['type' => $type, 'message' => $message, 'appointments' => $rows];
        }
    }

    if (schema_table_exists(table('appointment_services'))) {
        $conditions = [];
        $args = [$branchId, $endAt, $startAt, $excludeAppointmentId];
        if ($dentistUserId > 0) { $conditions[] = 'aps.dentist_user_id=?'; $args[] = $dentistUserId; }
        if ($inhouseProviderUserId > 0) { $conditions[] = 'aps.inhouse_provider_user_id=?'; $args[] = $inhouseProviderUserId; }
        if ($chairId > 0) { $conditions[] = 'aps.chair_id=?'; $args[] = $chairId; }
        elseif (trim($chairName) !== '') { $conditions[] = 'LOWER(TRIM(COALESCE(aps.chair_name,"")))=LOWER(TRIM(?))'; $args[] = trim($chairName); }
        if ($conditions) {
            $sql = 'SELECT a.id,a.status,aps.start_at,aps.end_at,aps.service_name,aps.provider_type,aps.chair_name '
                . 'FROM `' . table('appointment_services') . '` aps JOIN `' . $appointmentTable . '` a ON a.id=aps.appointment_id '
                . 'WHERE a.branch_id=? AND a.status NOT IN ("cancelled","no_show","completed") AND aps.status IN ("planned","completed") '
                . 'AND aps.start_at<? AND aps.end_at>? AND a.id<>? AND (' . implode(' OR ', $conditions) . ') '
                . 'ORDER BY aps.start_at ASC LIMIT 5';
            $stmt = $pdo->prepare($sql);
            $stmt->execute($args);
            $rows = $stmt->fetchAll();
            if ($rows) $details[] = ['type' => 'service_segment', 'message' => 'A service segment already uses the provider/chair during this time.', 'appointments' => $rows];
        }
    }

    return $details;
}

function appointment_guard_assert_schedule_available(PDO $pdo, int $branchId, int $patientId, int $dentistUserId, int $chairId, int $inhouseProviderUserId, string $chairName, string $startAt, string $endAt, int $excludeAppointmentId = 0, bool $bypass = false, string $bypassReason = ''): void
{
    $conflicts = appointment_guard_conflict_details($pdo, $branchId, $patientId, $dentistUserId, $chairId, $inhouseProviderUserId, $chairName, $startAt, $endAt, $excludeAppointmentId);
    if (!$conflicts) return;

    // Schedule Bypass may authorize an availability/template exception, but it
    // may never double-book a patient, dentist, in-house provider, chair, or
    // service segment. Hard resource conflicts always fail closed.
    appointment_guard_log_event($pdo, [
        'branch_id' => $branchId,
        'patient_id' => $patientId,
        'appointment_id' => $excludeAppointmentId ?: null,
        'event_type' => 'schedule_conflict_blocked',
        'event_label' => 'Hard schedule conflict blocked',
        'severity' => 'critical',
        'message' => 'Resource conflicts cannot be bypassed. Choose another time, chair, or provider.',
        'metadata' => [
            'conflicts' => $conflicts,
            'bypass_requested' => $bypass,
            'bypass_reason' => $bypass ? mb_substr(trim($bypassReason), 0, 500) : null,
        ],
    ]);

    $first = $conflicts[0]['message'] ?? 'The requested time conflicts with an existing appointment.';
    throw new RuntimeException((string)$first . ' Resource conflicts cannot be bypassed; choose another time, chair, or provider.');
}

function appointment_guard_assignment_status(array $appointment): string
{
    $providerType = (string)($appointment['provider_type'] ?? 'dentist');
    if ($providerType !== 'dentist') return 'not_required';
    return ((int)($appointment['dentist_user_id'] ?? 0) > 0) ? 'assigned' : 'needs_assignment';
}

function appointment_guard_sync_assignment(PDO $pdo, array $appointment, ?int $requestId = null, string $reason = ''): void
{
    if (!appointment_schedule_guard_schema_ready($pdo)) return;
    $appointmentId = (int)($appointment['id'] ?? 0);
    if ($appointmentId < 1) return;
    $branchId = (int)($appointment['branch_id'] ?? 0);
    $patientId = (int)($appointment['patient_id'] ?? 0);
    $status = appointment_guard_assignment_status($appointment);
    $note = $reason ?: ($status === 'needs_assignment' ? 'Dentist assignment required before confirmation or check-in.' : 'Provider assignment resolved.');

    $pdo->prepare('UPDATE `' . table('appointments') . '` SET assignment_status=?,assignment_note=?,assignment_requested_at=CASE WHEN ?="needs_assignment" AND assignment_requested_at IS NULL THEN NOW() ELSE assignment_requested_at END,assignment_resolved_at=CASE WHEN ? IN ("assigned","not_required") THEN NOW() ELSE assignment_resolved_at END WHERE id=?')
        ->execute([$status, mb_substr($note,0,500), $status, $status, $appointmentId]);

    if ($status === 'needs_assignment') {
        $stmt = $pdo->prepare(
            'INSERT INTO `' . table('appointment_assignment_queue') . '` '
            . '(branch_id,patient_id,appointment_id,appointment_request_id,assignment_type,priority,status,reason,requested_at,created_by,created_at,updated_at) '
            . 'VALUES (?,?,?,?,"clinic_decides",?,?,?,NOW(),?,NOW(),NOW()) '
            . 'ON DUPLICATE KEY UPDATE status=IF(status="resolved","resolved",VALUES(status)),reason=VALUES(reason),priority=VALUES(priority),updated_at=NOW()'
        );
        $priority = (strtotime((string)($appointment['start_at'] ?? '')) <= strtotime('+24 hours')) ? 'urgent' : 'normal';
        $stmt->execute([$branchId ?: null,$patientId ?: null,$appointmentId,$requestId ?: null,$priority,'pending',mb_substr($note,0,500),(int)(current_user()['id'] ?? 0) ?: null]);
        appointment_guard_log_event($pdo,[
            'branch_id'=>$branchId,'patient_id'=>$patientId,'appointment_id'=>$appointmentId,'appointment_request_id'=>$requestId,
            'event_type'=>'provider_assignment_required','event_label'=>'Provider assignment required','severity'=>'warning','message'=>$note,
        ]);
    } else {
        $pdo->prepare('UPDATE `' . table('appointment_assignment_queue') . '` SET status="resolved",resolved_by=?,resolved_at=NOW(),updated_at=NOW() WHERE appointment_id=? AND status<>"resolved"')
            ->execute([(int)(current_user()['id'] ?? 0) ?: null,$appointmentId]);
    }
}

function appointment_guard_mark_checked(PDO $pdo, int $appointmentId, string $status, string $message = ''): void
{
    if (!schema_column_exists(table('appointments'), 'schedule_guard_status')) return;
    $allowed = ['passed','conflict_blocked','bypass_approved','unchecked'];
    if (!in_array($status, $allowed, true)) $status = 'unchecked';
    $pdo->prepare('UPDATE `' . table('appointments') . '` SET schedule_guard_status=?,schedule_guard_message=?,schedule_guard_checked_at=NOW() WHERE id=?')
        ->execute([$status, mb_substr($message,0,500), $appointmentId]);
}

function appointment_guard_assert_status_change(PDO $pdo, array $appointment, string $newStatus, string $reason): void
{
    $current = (string)($appointment['status'] ?? '');
    $startAt = (string)($appointment['start_at'] ?? '');
    if ($newStatus === 'no_show') {
        if ($startAt !== '' && strtotime($startAt) > time()) {
            throw new RuntimeException('No-show can be marked only after the appointment time has passed. Use Cancel or Reschedule for future appointments.');
        }
        if (mb_strlen(trim($reason)) < 8) throw new RuntimeException('Enter a clear no-show note before saving.');
    }
    if ($newStatus === 'cancelled') {
        if (in_array($current, ['checked_in','in_treatment','awaiting_billing'], true)) {
            throw new RuntimeException('This patient is already in the active visit flow. Manage the cancellation from Queue or the clinical workspace.');
        }
        if (mb_strlen(trim($reason)) < 8) throw new RuntimeException('Enter a clear cancellation reason before saving.');
    }
}

function appointment_guard_pending_assignment_count(PDO $pdo, int $branchId = 0): int
{
    if (!schema_table_exists(table('appointment_assignment_queue'))) return 0;
    $sql = 'SELECT COUNT(*) FROM `' . table('appointment_assignment_queue') . '` WHERE status IN ("pending","in_review")';
    $args = [];
    if ($branchId > 0) { $sql .= ' AND branch_id=?'; $args[] = $branchId; }
    $stmt = $pdo->prepare($sql);
    $stmt->execute($args);
    return (int)$stmt->fetchColumn();
}

function appointment_guard_pending_assignments(PDO $pdo, int $branchId = 0, int $limit = 10): array
{
    if (!schema_table_exists(table('appointment_assignment_queue'))) return [];
    $limit = max(1, min(50, $limit));
    $sql = 'SELECT q.*,a.start_at,a.service_name,a.status appointment_status,TRIM(CONCAT_WS(" ",p.first_name,NULLIF(p.middle_name,""),p.last_name)) patient_name,p.patient_no '
        . 'FROM `' . table('appointment_assignment_queue') . '` q '
        . 'LEFT JOIN `' . table('appointments') . '` a ON a.id=q.appointment_id '
        . 'LEFT JOIN `' . table('patients') . '` p ON p.id=q.patient_id '
        . 'WHERE q.status IN ("pending","in_review")';
    $args = [];
    if ($branchId > 0) { $sql .= ' AND q.branch_id=?'; $args[] = $branchId; }
    $sql .= ' ORDER BY FIELD(q.priority,"urgent","normal","low"),COALESCE(a.start_at,q.requested_at),q.id LIMIT ' . $limit;
    $stmt = $pdo->prepare($sql);
    $stmt->execute($args);
    return $stmt->fetchAll();
}

function appointment_guard_request_assignment(PDO $pdo, int $requestId, int $branchId, int $patientId, string $reason): void
{
    if (!schema_table_exists(table('appointment_assignment_queue'))) return;
    $pdo->prepare('UPDATE `' . table('appointment_requests') . '` SET assignment_queue_status="pending",updated_at=NOW() WHERE id=?')->execute([$requestId]);
    appointment_guard_log_event($pdo,[
        'branch_id'=>$branchId,'patient_id'=>$patientId,'appointment_request_id'=>$requestId,
        'event_type'=>'request_provider_assignment_required','event_label'=>'Request needs provider assignment','severity'=>'warning','message'=>$reason,
    ]);
}
