<?php

declare(strict_types=1);

/**
 * Unified, role-aware patient timeline and vital-trend helpers.
 *
 * The timeline is assembled from authoritative source tables at read time. It
 * intentionally does not duplicate clinical or financial records into another
 * ledger, avoiding stale summaries and preserving original authorship.
 */

function patient_timeline_category_map(string $role): array
{
    $operational = [
        'appointment' => 'Appointments',
        'queue' => 'Queue and chair flow',
        'followup' => 'Follow-ups and recalls',
        'communication' => 'Communications',
    ];
    $clinical = [
        'encounter' => 'Clinical encounters',
        'vitals' => 'Vital signs',
        'medical' => 'Dental safety history',
        'diagnosis' => 'Diagnoses',
        'chart' => 'Dental chart',
        'plan' => 'Treatment plans',
        'procedure' => 'Procedures',
        'prescription' => 'Prescriptions',
        'document' => 'Images and documents',
        'consent' => 'Consents',
    ];
    $finance = [
        'invoice' => 'Invoices',
        'payment' => 'Payments',
    ];

    return match ($role) {
        'super_admin', 'clinic_admin' => $operational + $clinical + $finance,
        'dentist', 'dental_assistant' => $operational + $clinical,
        'receptionist' => $operational,
        default => [],
    };
}

function patient_timeline_safe_all(PDO $pdo, string $sql, array $args = []): array
{
    try {
        $stmt = $pdo->prepare($sql);
        $stmt->execute($args);
        return $stmt->fetchAll();
    } catch (Throwable $exception) {
        error_log('DentalCare patient timeline query failed: ' . $exception->getMessage());
        return [];
    }
}

function patient_timeline_excerpt(mixed $value, int $limit = 240): string
{
    $text = trim(preg_replace('/\s+/u', ' ', strip_tags((string)$value)) ?? '');
    if ($text === '') return '';
    return mb_strlen($text) > $limit ? mb_substr($text, 0, $limit - 1) . '…' : $text;
}

function patient_timeline_add(array &$events, array $event): void
{
    $at = trim((string)($event['at'] ?? ''));
    if ($at === '' || strtotime($at) === false) return;
    $events[] = [
        'at' => $at,
        'category' => (string)($event['category'] ?? 'encounter'),
        'title' => trim((string)($event['title'] ?? 'Record activity')),
        'summary' => patient_timeline_excerpt($event['summary'] ?? ''),
        'status' => trim((string)($event['status'] ?? '')),
        'actor' => trim((string)($event['actor'] ?? '')),
        'dentist' => trim((string)($event['dentist'] ?? '')),
        'assistant' => trim((string)($event['assistant'] ?? '')),
        'encounter_id' => max(0, (int)($event['encounter_id'] ?? 0)),
        'record_id' => max(0, (int)($event['record_id'] ?? 0)),
        'url' => trim((string)($event['url'] ?? '')),
        'flag' => trim((string)($event['flag'] ?? '')),
    ];
}

function patient_timeline_fetch(PDO $pdo, int $patientId, string $role, array $filters = []): array
{
    $allowed = patient_timeline_category_map($role);
    if (!$allowed || $patientId < 1) return [];

    $events = [];
    $base = 'patients.php?action=view&id=' . $patientId;

    if (isset($allowed['appointment']) && schema_table_exists(table('appointments'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT a.*,cu.name creator,(SELECT ce.id FROM `' . table('clinical_encounters') . '` ce WHERE ce.appointment_id=a.id ORDER BY ce.id DESC LIMIT 1) linked_encounter_id FROM `' . table('appointments') . '` a LEFT JOIN `' . table('users') . '` cu ON cu.id=a.created_by WHERE a.patient_id=? ORDER BY a.start_at DESC,a.id DESC LIMIT 150',
            [$patientId]
        );
        foreach ($rows as $row) {
            $provider = trim((string)($row['dentist_name'] ?: ($row['inhouse_provider_name'] ?? '')));
            $summary = trim(implode(' · ', array_filter([
                (string)($row['service_name'] ?? 'Dental visit'),
                $provider !== '' ? 'Provider: ' . $provider : null,
                !empty($row['chair_name']) ? 'Chair: ' . $row['chair_name'] : null,
                !empty($row['reason_for_visit']) ? (string)$row['reason_for_visit'] : null,
            ])));
            patient_timeline_add($events, [
                'at' => $row['start_at'], 'category' => 'appointment',
                'title' => 'Appointment — ' . (($row['service_name'] ?? '') ?: 'Dental visit'),
                'summary' => $summary, 'status' => $row['status'] ?? '', 'actor' => $row['creator'] ?? '',
                'dentist' => $provider, 'encounter_id' => $row['linked_encounter_id'] ?? 0, 'record_id' => $row['id'],
                'url' => $base . '&tab=appointments&section=' . (strtotime((string)$row['start_at']) >= time() ? 'upcoming' : 'past'),
            ]);
        }
    }

    if (isset($allowed['queue']) && schema_table_exists(table('queue_entries'))) {
        if (schema_table_exists(table('queue_status_history'))) {
            $rows = patient_timeline_safe_all($pdo,
                'SELECT h.*,q.queue_no,q.service_name,q.chair_name,q.dentist_name,u.name actor,(SELECT ce.id FROM `' . table('clinical_encounters') . '` ce WHERE ce.queue_entry_id=q.id ORDER BY ce.id DESC LIMIT 1) linked_encounter_id FROM `' . table('queue_status_history') . '` h JOIN `' . table('queue_entries') . '` q ON q.id=h.queue_entry_id LEFT JOIN `' . table('users') . '` u ON u.id=h.changed_by WHERE q.patient_id=? ORDER BY h.created_at DESC,h.id DESC LIMIT 200',
                [$patientId]
            );
            foreach ($rows as $row) {
                patient_timeline_add($events, [
                    'at' => $row['created_at'], 'category' => 'queue',
                    'title' => 'Queue status — ' . ucwords(str_replace('_', ' ', (string)$row['new_status'])),
                    'summary' => trim(implode(' · ', array_filter([
                        !empty($row['queue_no']) ? 'Ticket ' . $row['queue_no'] : null,
                        !empty($row['service_name']) ? (string)$row['service_name'] : null,
                        !empty($row['chair_name']) ? 'Chair ' . $row['chair_name'] : null,
                        !empty($row['notes']) ? (string)$row['notes'] : null,
                    ]))),
                    'status' => $row['new_status'] ?? '', 'actor' => $row['actor'] ?? '',
                    'dentist' => $row['dentist_name'] ?? '', 'encounter_id' => $row['linked_encounter_id'] ?? 0, 'record_id' => $row['queue_entry_id'],
                    'url' => 'visit_workflow.php?patient_id=' . $patientId,
                ]);
            }
        } else {
            $rows = patient_timeline_safe_all($pdo, 'SELECT q.*,(SELECT ce.id FROM `' . table('clinical_encounters') . '` ce WHERE ce.queue_entry_id=q.id ORDER BY ce.id DESC LIMIT 1) linked_encounter_id FROM `' . table('queue_entries') . '` q WHERE q.patient_id=? ORDER BY q.checked_in_at DESC,q.id DESC LIMIT 100', [$patientId]);
            foreach ($rows as $row) {
                patient_timeline_add($events, [
                    'at' => $row['checked_in_at'], 'category' => 'queue', 'title' => 'Queue check-in',
                    'summary' => trim((string)($row['queue_no'] ?? '') . ' · ' . (string)($row['service_name'] ?? 'Dental visit')),
                    'status' => $row['status'] ?? '', 'dentist' => $row['dentist_name'] ?? '', 'encounter_id' => $row['linked_encounter_id'] ?? 0, 'record_id' => $row['id'],
                    'url' => 'visit_workflow.php?patient_id=' . $patientId,
                ]);
            }
        }
    }

    if (isset($allowed['encounter']) && schema_table_exists(table('clinical_encounters'))) {
        $rows = patient_timeline_safe_all($pdo, 'SELECT ce.* FROM `' . table('clinical_encounters') . '` ce WHERE ce.patient_id=? ORDER BY ce.created_at DESC,ce.id DESC LIMIT 150', [$patientId]);
        foreach ($rows as $row) {
            patient_timeline_add($events, [
                'at' => $row['created_at'], 'category' => 'encounter', 'title' => 'Clinical encounter opened',
                'summary' => ($row['chief_complaint'] ?? '') ?: (($row['visit_type'] ?? '') ? 'Visit type: ' . $row['visit_type'] : 'Clinical record started'),
                'status' => $row['status'] ?? '', 'dentist' => $row['dentist_name'] ?? '', 'assistant' => $row['assistant_name'] ?? '',
                'encounter_id' => $row['id'], 'record_id' => $row['id'], 'url' => 'clinical_workspace.php?encounter_id=' . (int)$row['id'],
            ]);
            if (!empty($row['assistant_assigned_at']) && !empty($row['assistant_name'])) {
                patient_timeline_add($events, [
                    'at' => $row['assistant_assigned_at'], 'category' => 'encounter', 'title' => 'Dental Assistant assigned',
                    'summary' => 'Assigned to the active encounter.', 'status' => 'assigned', 'dentist' => $row['dentist_name'] ?? '',
                    'assistant' => $row['assistant_name'], 'encounter_id' => $row['id'], 'record_id' => $row['id'],
                    'url' => 'clinical_workspace.php?encounter_id=' . (int)$row['id'],
                ]);
            }
            if (!empty($row['paused_at'])) {
                patient_timeline_add($events, [
                    'at' => $row['paused_at'], 'category' => 'encounter', 'title' => 'Treatment paused',
                    'summary' => $row['pause_reason'] ?? '', 'status' => 'paused', 'dentist' => $row['dentist_name'] ?? '',
                    'assistant' => $row['assistant_name'] ?? '', 'encounter_id' => $row['id'], 'record_id' => $row['id'],
                    'url' => 'clinical_workspace.php?encounter_id=' . (int)$row['id'],
                ]);
            }
            if (!empty($row['finalized_at'])) {
                patient_timeline_add($events, [
                    'at' => $row['finalized_at'], 'category' => 'encounter', 'title' => 'Clinical encounter finalized',
                    'summary' => trim(implode(' · ', array_filter([
                        !empty($row['procedure_done']) ? (string)$row['procedure_done'] : null,
                        !empty($row['completion_destination']) ? 'Destination: ' . ucwords(str_replace('_', ' ', (string)$row['completion_destination'])) : null,
                        !empty($row['follow_up_purpose']) ? 'Follow-up: ' . $row['follow_up_purpose'] : null,
                    ]))),
                    'status' => 'finalized', 'dentist' => ($row['signed_by_name'] ?? '') ?: ($row['dentist_name'] ?? ''),
                    'assistant' => $row['assistant_name'] ?? '', 'encounter_id' => $row['id'], 'record_id' => $row['id'],
                    'url' => 'print_record.php?type=encounter&id=' . (int)$row['id'],
                ]);
            }
        }
    }

    if (isset($allowed['vitals']) && schema_table_exists(table('vital_signs'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT v.*,u.name actor,ce.dentist_name,ce.assistant_name FROM `' . table('vital_signs') . '` v LEFT JOIN `' . table('users') . '` u ON u.id=v.recorded_by LEFT JOIN `' . table('clinical_encounters') . '` ce ON ce.id=v.encounter_id WHERE v.patient_id=? ORDER BY v.recorded_at DESC,v.id DESC LIMIT 150',
            [$patientId]
        );
        $age = 0;
        try {
            $birth = patient_timeline_safe_all($pdo, 'SELECT birth_date FROM `' . table('patients') . '` WHERE id=? LIMIT 1', [$patientId]);
            if (!empty($birth[0]['birth_date'])) $age = (new DateTimeImmutable((string)$birth[0]['birth_date']))->diff(new DateTimeImmutable('today'))->y;
        } catch (Throwable) {}
        foreach ($rows as $row) {
            [$sys,$dia,$repeat] = clinical_vitals_effective_bp($row);
            $screen = clinical_vitals_bp_screening($row, $age);
            $parts = [];
            if ($sys !== null && $dia !== null) $parts[] = 'BP ' . $sys . '/' . $dia . ' mmHg' . ($repeat ? ' (repeat)' : '');
            if ($row['pulse_rate'] !== null) $parts[] = 'Pulse ' . $row['pulse_rate'] . ' bpm';
            if ($row['oxygen_saturation'] !== null) $parts[] = 'SpO₂ ' . $row['oxygen_saturation'] . '%';
            if ($row['temperature_c'] !== null) $parts[] = 'Temp ' . $row['temperature_c'] . ' °C';
            if ($row['weight_kg'] !== null) $parts[] = 'Weight ' . $row['weight_kg'] . ' kg';
            if ($row['bmi'] !== null) $parts[] = 'BMI ' . $row['bmi'];
            patient_timeline_add($events, [
                'at' => $row['recorded_at'], 'category' => 'vitals', 'title' => 'Vital signs recorded',
                'summary' => implode(' · ', $parts), 'status' => $screen['label'] ?? '', 'actor' => $row['actor'] ?? '',
                'dentist' => $row['dentist_name'] ?? '', 'assistant' => $row['assistant_name'] ?? '',
                'encounter_id' => $row['encounter_id'] ?? 0, 'record_id' => $row['id'],
                'url' => $base . '&tab=clinical&section=vitals', 'flag' => $screen['class'] ?? '',
            ]);
        }
    }

    if (isset($allowed['medical']) && schema_table_exists(table('medical_histories'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT m.*,u.name actor,vu.name verifier FROM `' . table('medical_histories') . '` m LEFT JOIN `' . table('users') . '` u ON u.id=m.created_by LEFT JOIN `' . table('users') . '` vu ON vu.id=m.verified_by WHERE m.patient_id=? ORDER BY m.created_at DESC,m.id DESC LIMIT 100',
            [$patientId]
        );
        foreach ($rows as $row) {
            $status = (string)($row['verification_status'] ?? (!empty($row['confirmed_at']) ? 'verified' : 'pending_review'));
            patient_timeline_add($events, [
                'at' => $row['verified_at'] ?: $row['created_at'], 'category' => 'medical',
                'title' => $status === 'verified' ? 'Dental safety history verified' : 'Dental safety history update recorded',
                'summary' => trim(implode(' · ', array_filter([
                    !empty($row['allergies']) ? 'Allergies: ' . $row['allergies'] : null,
                    !empty($row['conditions']) ? 'Conditions: ' . $row['conditions'] : null,
                    !empty($row['medications']) ? 'Medications: ' . $row['medications'] : null,
                ]))),
                'status' => $status, 'actor' => ($row['verifier'] ?? '') ?: ($row['actor'] ?? ''),
                'record_id' => $row['id'], 'url' => $base . '&tab=history&section=versions',
            ]);
        }
    }

    if (isset($allowed['diagnosis']) && schema_table_exists(table('diagnoses'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT d.*,u.name actor,ce.dentist_name,ce.assistant_name FROM `' . table('diagnoses') . '` d LEFT JOIN `' . table('users') . '` u ON u.id=d.diagnosed_by LEFT JOIN `' . table('clinical_encounters') . '` ce ON ce.id=d.encounter_id WHERE d.patient_id=? ORDER BY d.diagnosed_at DESC,d.id DESC LIMIT 150',
            [$patientId]
        );
        foreach ($rows as $row) patient_timeline_add($events, [
            'at' => $row['diagnosed_at'], 'category' => 'diagnosis', 'title' => 'Diagnosis — ' . $row['diagnosis_name'],
            'summary' => trim(implode(' · ', array_filter([
                !empty($row['tooth_region']) ? 'Tooth/region ' . $row['tooth_region'] : null,
                !empty($row['severity']) ? 'Severity: ' . $row['severity'] : null,
                $row['notes'] ?? null,
            ]))),
            'status' => $row['diagnosis_status'] ?? '', 'actor' => $row['actor'] ?? '', 'dentist' => $row['dentist_name'] ?? '',
            'assistant' => $row['assistant_name'] ?? '', 'encounter_id' => $row['encounter_id'], 'record_id' => $row['id'],
            'url' => $base . '&tab=treatment&section=diagnoses',
        ]);
    }

    if (isset($allowed['chart']) && schema_table_exists(table('odontogram_entries'))) {
        $conditionJoin = schema_table_exists(table('dental_condition_catalog'))
            ? ' LEFT JOIN `' . table('dental_condition_catalog') . '` c ON c.code=o.condition_code '
            : '';
        $conditionSelect = schema_table_exists(table('dental_condition_catalog')) ? ',c.name condition_name' : ',NULL condition_name';
        $rows = patient_timeline_safe_all($pdo,
            'SELECT o.*,u.name actor' . $conditionSelect . ' FROM `' . table('odontogram_entries') . '` o LEFT JOIN `' . table('users') . '` u ON u.id=o.recorded_by ' . $conditionJoin . ' WHERE o.patient_id=? ORDER BY o.recorded_at DESC,o.id DESC LIMIT 200',
            [$patientId]
        );
        foreach ($rows as $row) patient_timeline_add($events, [
            'at' => $row['recorded_at'], 'category' => 'chart', 'title' => 'Dental chart — Tooth ' . $row['tooth_number'],
            'summary' => trim(implode(' · ', array_filter([
                ($row['condition_name'] ?? '') ?: ucwords(str_replace('_', ' ', (string)$row['condition_code'])),
                !empty($row['surface']) ? 'Surface ' . $row['surface'] : null,
                !empty($row['severity']) ? 'Severity ' . $row['severity'] : null,
                $row['notes'] ?? null,
            ]))),
            'status' => $row['status'] ?? '', 'actor' => $row['actor'] ?? '', 'encounter_id' => $row['encounter_id'] ?? 0,
            'record_id' => $row['id'], 'url' => $base . '&tab=clinical&section=odontogram',
        ]);
    }

    if (isset($allowed['plan']) && schema_table_exists(table('treatment_plans'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT tp.*,u.name dentist_name FROM `' . table('treatment_plans') . '` tp LEFT JOIN `' . table('users') . '` u ON u.id=tp.dentist_user_id WHERE tp.patient_id=? ORDER BY tp.created_at DESC,tp.id DESC LIMIT 100',
            [$patientId]
        );
        foreach ($rows as $row) patient_timeline_add($events, [
            'at' => $row['updated_at'] ?: $row['created_at'], 'category' => 'plan', 'title' => 'Treatment plan — ' . $row['title'],
            'summary' => trim(implode(' · ', array_filter([
                !empty($row['phase']) ? 'Phase ' . $row['phase'] : null,
                'Version ' . ($row['version_no'] ?? 1),
                $row['notes'] ?? null,
            ]))),
            'status' => $row['status'] ?? '', 'dentist' => $row['dentist_name'] ?? '', 'encounter_id' => $row['encounter_id'] ?? 0,
            'record_id' => $row['id'], 'url' => $base . '&tab=treatment&section=plans',
        ]);
    }

    if (isset($allowed['procedure']) && schema_table_exists(table('procedures'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT pr.*,du.name dentist_name,au.name assistant_name,eu.name actor,vu.name verifier FROM `' . table('procedures') . '` pr LEFT JOIN `' . table('users') . '` du ON du.id=pr.dentist_user_id LEFT JOIN `' . table('users') . '` au ON au.id=pr.assistant_user_id LEFT JOIN `' . table('users') . '` eu ON eu.id=pr.entered_by_user_id LEFT JOIN `' . table('users') . '` vu ON vu.id=pr.verified_by_user_id WHERE pr.patient_id=? ORDER BY COALESCE(pr.ended_at,pr.started_at,pr.created_at) DESC,pr.id DESC LIMIT 200',
            [$patientId]
        );
        foreach ($rows as $row) {
            $verified = !empty($row['verified_by_user_id']);
            patient_timeline_add($events, [
                'at' => $row['ended_at'] ?: ($row['started_at'] ?: $row['created_at']), 'category' => 'procedure',
                'title' => 'Procedure — ' . $row['procedure_name'],
                'summary' => trim(implode(' · ', array_filter([
                    !empty($row['tooth_region']) ? 'Tooth/region ' . $row['tooth_region'] : null,
                    !empty($row['materials']) ? 'Materials: ' . $row['materials'] : null,
                    !empty($row['outcome']) ? (string)$row['outcome'] : null,
                    !empty($row['complications']) ? 'Complications: ' . $row['complications'] : null,
                ]))),
                'status' => ($row['entered_by_role'] ?? '') === 'dental_assistant' && !$verified ? 'awaiting_dentist_verification' : ($row['status'] ?? ''),
                'actor' => $row['actor'] ?? '', 'dentist' => $row['dentist_name'] ?? '', 'assistant' => $row['assistant_name'] ?? '',
                'encounter_id' => $row['encounter_id'] ?? 0, 'record_id' => $row['id'], 'url' => $base . '&tab=treatment&section=procedures',
                'flag' => (($row['entered_by_role'] ?? '') === 'dental_assistant' && !$verified) ? 'warning' : '',
            ]);
        }
    }

    if (isset($allowed['prescription']) && schema_table_exists(table('prescriptions'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT rx.*,u.name dentist_name,(SELECT GROUP_CONCAT(TRIM(CONCAT(pi.medication_name," ",COALESCE(pi.strength,""))) ORDER BY pi.id SEPARATOR ", ") FROM `' . table('prescription_items') . '` pi WHERE pi.prescription_id=rx.id) medicines FROM `' . table('prescriptions') . '` rx LEFT JOIN `' . table('users') . '` u ON u.id=rx.dentist_user_id WHERE rx.patient_id=? ORDER BY rx.issued_at DESC,rx.id DESC LIMIT 100',
            [$patientId]
        );
        foreach ($rows as $row) patient_timeline_add($events, [
            'at' => $row['issued_at'] ?: $row['created_at'], 'category' => 'prescription', 'title' => 'Prescription — ' . $row['prescription_no'],
            'summary' => ($row['medicines'] ?? '') ?: ($row['clinical_indication'] ?? 'Prescription record'), 'status' => $row['status'] ?? '',
            'dentist' => ($row['signed_by_name'] ?? '') ?: ($row['dentist_name'] ?? ''), 'encounter_id' => $row['encounter_id'] ?? 0,
            'record_id' => $row['id'], 'url' => in_array($role, ['super_admin','clinic_admin','dentist'], true) ? 'print_record.php?type=prescription&id=' . (int)$row['id'] : $base . '&tab=treatment&section=prescriptions',
        ]);
    }

    if (isset($allowed['document']) && schema_table_exists(table('documents'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT d.*,u.name actor,ru.name reviewer FROM `' . table('documents') . '` d LEFT JOIN `' . table('users') . '` u ON u.id=d.uploaded_by LEFT JOIN `' . table('users') . '` ru ON ru.id=d.reviewed_by WHERE d.patient_id=? ORDER BY d.created_at DESC,d.id DESC LIMIT 150',
            [$patientId]
        );
        foreach ($rows as $row) patient_timeline_add($events, [
            'at' => $row['created_at'], 'category' => 'document', 'title' => 'Document — ' . $row['category'],
            'summary' => trim(implode(' · ', array_filter([(string)$row['file_name'], $row['tooth_region'] ?? null, $row['description'] ?? null]))),
            'status' => !empty($row['reviewed_at']) ? 'reviewed' : 'uploaded', 'actor' => $row['actor'] ?? '',
            'encounter_id' => $row['encounter_id'] ?? 0, 'record_id' => $row['id'], 'url' => 'download_document.php?id=' . (int)$row['id'],
        ]);
    }

    if (isset($allowed['consent']) && schema_table_exists(table('consent_forms'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT c.*,u.name witness FROM `' . table('consent_forms') . '` c LEFT JOIN `' . table('users') . '` u ON u.id=c.witness_user_id WHERE c.patient_id=? ORDER BY COALESCE(c.signed_at,c.created_at) DESC,c.id DESC LIMIT 100',
            [$patientId]
        );
        foreach ($rows as $row) patient_timeline_add($events, [
            'at' => $row['signed_at'] ?: $row['created_at'], 'category' => 'consent', 'title' => 'Consent — ' . $row['consent_type'],
            'summary' => trim(implode(' · ', array_filter([
                !empty($row['signer_name']) ? 'Signer: ' . $row['signer_name'] : null,
                !empty($row['signer_relationship']) ? (string)$row['signer_relationship'] : null,
                !empty($row['witness']) ? 'Witness: ' . $row['witness'] : null,
                $row['decline_reason'] ?? null,
            ]))),
            'status' => $row['status'] ?? '', 'actor' => $row['witness'] ?? '', 'encounter_id' => $row['encounter_id'] ?? 0,
            'record_id' => $row['id'], 'url' => $base . '&tab=documents&section=consents',
        ]);
    }

    if (isset($allowed['followup'])) {
        if (schema_table_exists(table('follow_ups'))) {
            $rows = patient_timeline_safe_all($pdo,
                'SELECT f.*,u.name dentist_name FROM `' . table('follow_ups') . '` f LEFT JOIN `' . table('users') . '` u ON u.id=f.dentist_user_id WHERE f.patient_id=? ORDER BY f.created_at DESC,f.id DESC LIMIT 100',
                [$patientId]
            );
            foreach ($rows as $row) patient_timeline_add($events, [
                'at' => $row['scheduled_at'] ?: $row['created_at'], 'category' => 'followup', 'title' => 'Follow-up — ' . $row['purpose'],
                'summary' => 'Recommended ' . $row['recommended_date'] . (!empty($row['instructions']) ? ' · ' . $row['instructions'] : ''),
                'status' => $row['status'] ?? '', 'dentist' => $row['dentist_name'] ?? '', 'encounter_id' => $row['encounter_id'] ?? 0,
                'record_id' => $row['id'], 'url' => $base . '&tab=appointments&section=followups',
            ]);
        }
        if (schema_table_exists(table('recalls'))) {
            $rows = patient_timeline_safe_all($pdo, 'SELECT * FROM `' . table('recalls') . '` WHERE patient_id=? ORDER BY created_at DESC,id DESC LIMIT 100', [$patientId]);
            foreach ($rows as $row) patient_timeline_add($events, [
                'at' => $row['created_at'], 'category' => 'followup', 'title' => 'Recall — ' . $row['recall_type'],
                'summary' => 'Due ' . $row['due_date'] . (!empty($row['notes']) ? ' · ' . $row['notes'] : ''), 'status' => $row['status'] ?? '',
                'record_id' => $row['id'], 'url' => $base . '&tab=appointments&section=recalls',
            ]);
        }
    }

    if (isset($allowed['communication']) && schema_table_exists(table('communications'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT c.*,u.name actor FROM `' . table('communications') . '` c LEFT JOIN `' . table('users') . '` u ON u.id=c.user_id WHERE c.patient_id=? ORDER BY c.communicated_at DESC,c.id DESC LIMIT 150',
            [$patientId]
        );
        foreach ($rows as $row) patient_timeline_add($events, [
            'at' => $row['communicated_at'], 'category' => 'communication',
            'title' => ucwords(str_replace('_', ' ', (string)$row['channel'])) . ' — ' . (($row['subject'] ?? '') ?: 'Patient communication'),
            'summary' => $row['summary'] ?? '', 'status' => $row['delivery_status'] ?? '', 'actor' => $row['actor'] ?? '',
            'record_id' => $row['id'], 'url' => $base . '&tab=communications&section=all',
        ]);
    }

    if (isset($allowed['invoice']) && schema_table_exists(table('invoices'))) {
        $rows = patient_timeline_safe_all($pdo, 'SELECT i.* FROM `' . table('invoices') . '` i WHERE i.patient_id=? ORDER BY i.created_at DESC,i.id DESC LIMIT 150', [$patientId]);
        foreach ($rows as $row) patient_timeline_add($events, [
            'at' => $row['created_at'], 'category' => 'invoice', 'title' => 'Invoice — ' . $row['invoice_no'],
            'summary' => trim((string)$row['description']) . ' · ₱' . number_format((float)$row['amount'], 2),
            'status' => $row['status'] ?? '', 'encounter_id' => $row['encounter_id'] ?? 0, 'record_id' => $row['id'],
            'url' => $base . '&tab=billing&section=invoices',
        ]);
    }

    if (isset($allowed['payment']) && schema_table_exists(table('payments'))) {
        $rows = patient_timeline_safe_all($pdo,
            'SELECT py.*,i.invoice_no,i.encounter_id,u.name actor FROM `' . table('payments') . '` py JOIN `' . table('invoices') . '` i ON i.id=py.invoice_id LEFT JOIN `' . table('users') . '` u ON u.id=py.received_by WHERE i.patient_id=? ORDER BY py.created_at DESC,py.id DESC LIMIT 150',
            [$patientId]
        );
        foreach ($rows as $row) patient_timeline_add($events, [
            'at' => $row['created_at'], 'category' => 'payment', 'title' => 'Payment — ' . $row['invoice_no'],
            'summary' => '₱' . number_format((float)$row['amount'], 2) . ' · ' . ucwords(str_replace('_', ' ', (string)$row['method'])) . (!empty($row['reference_no']) ? ' · Ref ' . $row['reference_no'] : ''),
            'status' => $row['status'] ?? 'posted', 'actor' => $row['actor'] ?? '', 'encounter_id' => $row['encounter_id'] ?? 0, 'record_id' => $row['id'],
            'url' => $base . '&tab=billing&section=payments',
        ]);
    }

    $category = trim((string)($filters['category'] ?? ''));
    $encounterId = max(0, (int)($filters['encounter_id'] ?? 0));
    $from = trim((string)($filters['from'] ?? ''));
    $to = trim((string)($filters['to'] ?? ''));
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $from) || strtotime($from) === false) $from = '';
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $to) || strtotime($to) === false) $to = '';
    $fromTs = $from !== '' ? strtotime($from . ' 00:00:00') : false;
    $toTs = $to !== '' ? strtotime($to . ' 23:59:59') : false;

    $events = array_values(array_filter($events, static function (array $event) use ($allowed,$category,$encounterId,$fromTs,$toTs): bool {
        if (!isset($allowed[$event['category']])) return false;
        if ($category !== '' && $event['category'] !== $category) return false;
        if ($encounterId > 0 && (int)$event['encounter_id'] !== $encounterId) return false;
        $ts = strtotime((string)$event['at']);
        if ($fromTs !== false && $ts < $fromTs) return false;
        if ($toTs !== false && $ts > $toTs) return false;
        return true;
    }));

    usort($events, static fn(array $a, array $b): int => (strtotime((string)$b['at']) <=> strtotime((string)$a['at'])) ?: ((int)$b['record_id'] <=> (int)$a['record_id']));
    return array_slice($events, 0, 500);
}

function patient_vital_metric_value(array $row, string $metric): ?float
{
    if ($metric === 'blood_pressure_systolic' || $metric === 'blood_pressure_diastolic') {
        [$sys,$dia] = clinical_vitals_effective_bp($row);
        return $metric === 'blood_pressure_systolic' ? ($sys !== null ? (float)$sys : null) : ($dia !== null ? (float)$dia : null);
    }
    if (!array_key_exists($metric, $row) || $row[$metric] === null || $row[$metric] === '') return null;
    return is_numeric($row[$metric]) ? (float)$row[$metric] : null;
}

function patient_vital_trends(array $vitals): array
{
    $definitions = [
        'blood_pressure' => ['label'=>'Blood pressure','unit'=>'mmHg','metrics'=>['blood_pressure_systolic','blood_pressure_diastolic'],'decimals'=>0],
        'pulse_rate' => ['label'=>'Pulse','unit'=>'bpm','metrics'=>['pulse_rate'],'decimals'=>0],
        'oxygen_saturation' => ['label'=>'Oxygen saturation','unit'=>'%','metrics'=>['oxygen_saturation'],'decimals'=>0],
        'temperature_c' => ['label'=>'Temperature','unit'=>'°C','metrics'=>['temperature_c'],'decimals'=>1],
        'weight_kg' => ['label'=>'Weight','unit'=>'kg','metrics'=>['weight_kg'],'decimals'=>1],
        'bmi' => ['label'=>'BMI','unit'=>'','metrics'=>['bmi'],'decimals'=>1],
        'pain_score' => ['label'=>'Pain score','unit'=>'/10','metrics'=>['pain_score'],'decimals'=>0],
        'blood_glucose' => ['label'=>'Blood glucose','unit'=>'mg/dL','metrics'=>['blood_glucose'],'decimals'=>0],
    ];
    $out = [];
    foreach ($definitions as $key => $definition) {
        $values = [];
        foreach ($vitals as $row) {
            $metricValues = [];
            $complete = true;
            foreach ($definition['metrics'] as $metric) {
                $value = patient_vital_metric_value($row, $metric);
                if ($value === null) { $complete = false; break; }
                $metricValues[] = $value;
            }
            if (!$complete) continue;
            $values[] = ['values'=>$metricValues,'at'=>(string)($row['recorded_at'] ?? ''),'row'=>$row];
            if (count($values) >= 2) break;
        }
        $current = $values[0] ?? null;
        $previous = $values[1] ?? null;
        $format = static function (?array $entry) use ($definition): string {
            if (!$entry) return 'Not recorded';
            $parts = array_map(static fn(float $value): string => number_format($value, (int)$definition['decimals'], '.', ''), $entry['values']);
            return implode('/', $parts) . ($definition['unit'] !== '' ? ' ' . $definition['unit'] : '');
        };
        $delta = null;
        if ($current && $previous && count($current['values']) === 1) $delta = $current['values'][0] - $previous['values'][0];
        if ($current && $previous && count($current['values']) === 2) {
            $delta = [$current['values'][0]-$previous['values'][0],$current['values'][1]-$previous['values'][1]];
        }
        $out[$key] = [
            'label' => $definition['label'], 'current' => $format($current), 'previous' => $format($previous),
            'current_at' => $current['at'] ?? '', 'previous_at' => $previous['at'] ?? '', 'delta' => $delta,
            'has_current' => $current !== null,
        ];
    }
    return $out;
}

function patient_vital_delta_label(mixed $delta, string $unit = ''): string
{
    if ($delta === null) return 'No previous reading';
    if (is_array($delta)) {
        $a = (float)($delta[0] ?? 0); $b = (float)($delta[1] ?? 0);
        return sprintf('%+g/%+g %s from previous', $a, $b, $unit);
    }
    return sprintf('%+g%s from previous', (float)$delta, $unit !== '' ? ' ' . $unit : '');
}
