<?php

declare(strict_types=1);

/**
 * Transaction-integrity helpers introduced in v0.5.9.5.
 *
 * These helpers deliberately avoid UI concerns. They centralize record scope,
 * clinical verification, appointment state transitions, resource locking, and
 * appointment/queue synchronization so direct POST requests cannot bypass the
 * same controls enforced by the screens.
 */

function integrity_user_branch_id(): int
{
    return signed_user_branch_id();
}

function integrity_branch_scope_allowed(?int $recordBranchId, ?int $patientBranchId = null): bool
{
    if (active_role() === 'super_admin' && has_role('super_admin')) return true;
    $userBranchId = integrity_user_branch_id();
    if ($userBranchId < 1) return has_role(['super_admin','clinic_admin']);
    $targetBranchId = (int) ($recordBranchId ?: $patientBranchId ?: 0);
    return $targetBranchId > 0 && $targetBranchId === $userBranchId;
}

function integrity_document_category_is_clinical(string $category): bool
{
    $normalized = strtolower(trim($category));
    if ($normalized === '') return true;
    foreach (['x-ray','xray','radiograph','periapical','bitewing','panoramic','cephalometric','occlusal','cbct','clinical photograph','intraoral','extraoral','laboratory result'] as $needle) {
        if (str_contains($normalized, $needle)) return true;
    }
    return false;
}

/** @return array{allowed:bool,reason:string} */
function integrity_document_staff_decision(array $document): array
{
    $role = active_role();
    $patientId = (int) ($document['patient_id'] ?? 0);
    $recordBranchId = isset($document['branch_id']) ? (int) $document['branch_id'] : 0;
    $patientBranchId = isset($document['patient_branch_id']) ? (int) $document['patient_branch_id'] : 0;
    $accessLevel = (string) ($document['access_level'] ?? 'clinical');
    $category = (string) ($document['category'] ?? '');

    if (!in_array($role, ['super_admin','clinic_admin','dentist','dental_assistant','receptionist','staff'], true)) {
        return ['allowed'=>false,'reason'=>'The active role is not authorized to open patient clinical documents.'];
    }
    if (!integrity_branch_scope_allowed($recordBranchId, $patientBranchId)) {
        return ['allowed'=>false,'reason'=>'The document belongs to another clinic branch.'];
    }
    if ($patientId < 1) {
        return ['allowed'=>false,'reason'=>'The document is not linked to a valid patient.'];
    }
    if (in_array($role, ['dentist','dental_assistant'], true) && !task_patient_scope_allowed($patientId)) {
        return ['allowed'=>false,'reason'=>'The patient is outside the active clinical assignment.'];
    }
    if (in_array($role, ['receptionist','staff'], true)) {
        $allowedCategories = ['medical clearance','referral letter','consent form'];
        if (!in_array($accessLevel, ['staff','patient'], true)) {
            return ['allowed'=>false,'reason'=>'This file is restricted to the clinical team.'];
        }
        if (integrity_document_category_is_clinical($category) || !in_array(strtolower(trim($category)), $allowedCategories, true)) {
            return ['allowed'=>false,'reason'=>'Front-desk roles may open only explicitly shared clearance, referral, or consent documents.'];
        }
    }
    return ['allowed'=>true,'reason'=>''];
}

function integrity_medical_columns_ready(): bool
{
    $table = table('medical_histories');
    return schema_column_exists($table,'source_type')
        && schema_column_exists($table,'verification_status')
        && schema_column_exists($table,'verified_by')
        && schema_column_exists($table,'verified_at');
}

function integrity_insert_reported_medical_history(PDO $pdo, int $patientId, array $values, string $sourceType, ?int $createdBy, string $contextNote = ''): int
{
    $allergies = function_exists('dentalcare_normalize_medical_text') ? dentalcare_normalize_medical_text($values['allergies'] ?? '') : (trim((string) ($values['allergies'] ?? '')) ?: null);
    $medications = function_exists('dentalcare_normalize_medical_text') ? dentalcare_normalize_medical_text($values['medications'] ?? '') : (trim((string) ($values['medications'] ?? '')) ?: null);
    $conditions = function_exists('dentalcare_normalize_medical_text') ? dentalcare_normalize_medical_text($values['conditions'] ?? '') : (trim((string) ($values['conditions'] ?? '')) ?: null);
    $dentalHistory = function_exists('dentalcare_normalize_medical_text') ? dentalcare_normalize_medical_text($values['dental_history'] ?? '') : (trim((string) ($values['dental_history'] ?? '')) ?: null);

    if (integrity_medical_columns_ready()) {
        $stmt = $pdo->prepare(
            'INSERT INTO `' . table('medical_histories') . '` '
            . '(patient_id,allergies,medications,conditions,dental_history,confirmed_at,source_type,verification_status,verification_note,created_by,created_at) '
            . 'VALUES (?,?,?,?,?,NULL,?,"pending_review",?,?,NOW())'
        );
        $stmt->execute([$patientId,$allergies,$medications,$conditions,$dentalHistory,$sourceType,trim($contextNote) ?: null,$createdBy]);
    } else {
        $stmt = $pdo->prepare(
            'INSERT INTO `' . table('medical_histories') . '` '
            . '(patient_id,allergies,medications,conditions,dental_history,confirmed_at,created_by,created_at) '
            . 'VALUES (?,?,?,?,?,NULL,?,NOW())'
        );
        $stmt->execute([$patientId,$allergies,$medications,$conditions,$dentalHistory,$createdBy]);
    }
    return (int) $pdo->lastInsertId();
}

function integrity_insert_verified_medical_history(PDO $pdo, int $patientId, array $values, int $dentistUserId, string $verificationNote = ''): int
{
    if ($dentistUserId < 1 || !has_role('dentist') || active_role() !== 'dentist') {
        throw new RuntimeException('Only the signed-in dentist in the Dentist task view can verify health information relevant to dental care.');
    }
    $allergies = function_exists('dentalcare_normalize_medical_text') ? dentalcare_normalize_medical_text($values['allergies'] ?? '') : (trim((string) ($values['allergies'] ?? '')) ?: null);
    $medications = function_exists('dentalcare_normalize_medical_text') ? dentalcare_normalize_medical_text($values['medications'] ?? '') : (trim((string) ($values['medications'] ?? '')) ?: null);
    $conditions = function_exists('dentalcare_normalize_medical_text') ? dentalcare_normalize_medical_text($values['conditions'] ?? '') : (trim((string) ($values['conditions'] ?? '')) ?: null);
    $dentalHistory = function_exists('dentalcare_normalize_medical_text') ? dentalcare_normalize_medical_text($values['dental_history'] ?? '') : (trim((string) ($values['dental_history'] ?? '')) ?: null);

    if (integrity_medical_columns_ready()) {
        $stmt = $pdo->prepare(
            'INSERT INTO `' . table('medical_histories') . '` '
            . '(patient_id,allergies,medications,conditions,dental_history,confirmed_at,source_type,verification_status,verified_by,verified_at,verification_note,created_by,created_at) '
            . 'VALUES (?,?,?,?,?,NOW(),"clinician_verified","verified",?,NOW(),?,?,NOW())'
        );
        $stmt->execute([$patientId,$allergies,$medications,$conditions,$dentalHistory,$dentistUserId,trim($verificationNote) ?: null,$dentistUserId]);
    } else {
        $stmt = $pdo->prepare(
            'INSERT INTO `' . table('medical_histories') . '` '
            . '(patient_id,allergies,medications,conditions,dental_history,confirmed_at,created_by,created_at) '
            . 'VALUES (?,?,?,?,?,NOW(),?,NOW())'
        );
        $stmt->execute([$patientId,$allergies,$medications,$conditions,$dentalHistory,$dentistUserId]);
    }
    return (int) $pdo->lastInsertId();
}

function integrity_medical_alert_summary(array $values): ?string
{
    $summary = implode("\n", array_filter([
        function_exists('dentalcare_medical_alert_line') ? dentalcare_medical_alert_line('Allergies', $values['allergies'] ?? '') : (trim((string) ($values['allergies'] ?? '')) !== '' ? 'Allergies: ' . trim((string) $values['allergies']) : ''),
        function_exists('dentalcare_medical_alert_line') ? dentalcare_medical_alert_line('Medications', $values['medications'] ?? '') : (trim((string) ($values['medications'] ?? '')) !== '' ? 'Medications: ' . trim((string) $values['medications']) : ''),
        function_exists('dentalcare_medical_alert_line') ? dentalcare_medical_alert_line('Conditions', $values['conditions'] ?? '') : (trim((string) ($values['conditions'] ?? '')) !== '' ? 'Conditions: ' . trim((string) $values['conditions']) : ''),
    ]));
    return $summary !== '' ? $summary : null;
}

function integrity_guardian_columns_ready(): bool
{
    $table = table('patient_guardians');
    return schema_column_exists($table,'verification_status')
        && schema_column_exists($table,'verified_by')
        && schema_column_exists($table,'verified_at');
}

function integrity_insert_pending_guardian(PDO $pdo, int $patientId, array $values): int
{
    $args = [
        $patientId,
        trim((string) ($values['name'] ?? '')),
        trim((string) ($values['relationship'] ?? '')) ?: null,
        trim((string) ($values['mobile'] ?? '')) ?: null,
        trim((string) ($values['email'] ?? '')) ?: null,
        trim((string) ($values['address'] ?? '')) ?: null,
    ];
    if ($args[1] === '') throw new RuntimeException('Guardian name is required.');
    if (integrity_guardian_columns_ready()) {
        $stmt = $pdo->prepare(
            'INSERT INTO `' . table('patient_guardians') . '` '
            . '(patient_id,name,relationship,mobile,email,address,authorized_to_consent,authorized_to_receive_records,verification_status,created_at) '
            . 'VALUES (?,?,?,?,?,?,0,0,"pending",NOW())'
        );
    } else {
        $stmt = $pdo->prepare(
            'INSERT INTO `' . table('patient_guardians') . '` '
            . '(patient_id,name,relationship,mobile,email,address,authorized_to_consent,authorized_to_receive_records,created_at) '
            . 'VALUES (?,?,?,?,?,?,0,0,NOW())'
        );
    }
    $stmt->execute($args);
    return (int) $pdo->lastInsertId();
}

function integrity_verify_guardian(PDO $pdo, int $guardianId, int $patientId, bool $canConsent, bool $canReceiveRecords, string $verificationMethod, string $verificationNote): void
{
    if (!has_role(['super_admin','clinic_admin']) || !in_array(active_role(), ['super_admin','clinic_admin'], true)) {
        throw new RuntimeException('Guardian authority verification requires an Administrator task view.');
    }
    if (trim($verificationMethod) === '' || trim($verificationNote) === '') {
        throw new RuntimeException('Record the verification method and supporting note before granting guardian authority.');
    }
    $stmt = $pdo->prepare('SELECT * FROM `' . table('patient_guardians') . '` WHERE id=? AND patient_id=? FOR UPDATE');
    $stmt->execute([$guardianId,$patientId]);
    if (!$stmt->fetch()) throw new RuntimeException('Guardian record not found.');
    if (integrity_guardian_columns_ready()) {
        $pdo->prepare(
            'UPDATE `' . table('patient_guardians') . '` SET authorized_to_consent=?,authorized_to_receive_records=?,verification_status="verified",verification_method=?,verification_note=?,verified_by=?,verified_at=NOW(),revoked_at=NULL,updated_at=NOW() WHERE id=?'
        )->execute([$canConsent?1:0,$canReceiveRecords?1:0,trim($verificationMethod),trim($verificationNote),(int)(current_user()['id']??0),$guardianId]);
    } else {
        throw new RuntimeException('Apply the v0.5.9.5 database update before verifying guardian authority.');
    }
}

function integrity_revoke_guardian_authority(PDO $pdo, int $guardianId, int $patientId, string $reason): void
{
    if (!has_role(['super_admin','clinic_admin']) || !in_array(active_role(), ['super_admin','clinic_admin'], true)) {
        throw new RuntimeException('Guardian authority revocation requires an Administrator task view.');
    }
    $reason = trim($reason);
    if ($reason === '') throw new RuntimeException('Record the reason for revoking guardian authority.');
    if (!integrity_guardian_columns_ready()) {
        throw new RuntimeException('Apply the guardian authority database update before revoking access.');
    }
    $stmt = $pdo->prepare('SELECT id FROM `' . table('patient_guardians') . '` WHERE id=? AND patient_id=? FOR UPDATE');
    $stmt->execute([$guardianId,$patientId]);
    if (!$stmt->fetchColumn()) throw new RuntimeException('Guardian record not found.');
    $pdo->prepare(
        'UPDATE `' . table('patient_guardians') . '` SET authorized_to_consent=0,authorized_to_receive_records=0,verification_status="revoked",verification_method="Authority revoked",verification_note=?,revoked_at=NOW(),updated_at=NOW() WHERE id=?'
    )->execute([$reason,$guardianId]);
    if (schema_table_exists(table('patient_portal_account_patients'))) {
        $pdo->prepare('UPDATE `' . table('patient_portal_account_patients') . '` SET status="revoked",revoked_at=NOW(),updated_at=NOW() WHERE patient_id=? AND guardian_id=? AND is_self=0')
            ->execute([$patientId,$guardianId]);
    }
}

function integrity_appointment_transition_allowed(string $from, string $to, ?string $startAt = null): bool
{
    if ($from === $to) return true;
    $map = [
        'requested' => ['pending_confirmation','confirmed','cancelled'],
        'pending_confirmation' => ['confirmed','cancelled'],
        'confirmed' => ['pending_confirmation','cancelled','no_show','checked_in'],
        'checked_in' => ['called','seated','in_treatment','awaiting_billing','cancelled'],
        'called' => ['waiting','seated','cancelled'],
        'seated' => ['called','in_treatment','cancelled'],
        'in_treatment' => ['awaiting_billing','waiting_billing','completed','cancelled'],
        'awaiting_billing' => ['completed','cancelled'],
        'waiting_billing' => ['cashier_called','cancelled'],
        'cashier_called' => ['waiting_billing','completed','cancelled'],
        'completed' => [],
        'cancelled' => [],
        'no_show' => [],
    ];
    if (!in_array($to, $map[$from] ?? [], true)) return false;
    if ($to === 'no_show' && $startAt !== null && strtotime($startAt) > time()) return false;
    return true;
}

function integrity_assert_appointment_transition(string $from, string $to, ?string $startAt = null): void
{
    if (!integrity_appointment_transition_allowed($from,$to,$startAt)) {
        throw new RuntimeException('Invalid appointment transition from ' . str_replace('_',' ',$from) . ' to ' . str_replace('_',' ',$to) . '.');
    }
}

function integrity_lock_appointment(PDO $pdo, int $appointmentId, int $branchId): array
{
    $stmt = $pdo->prepare(
        'SELECT a.*,CONCAT(p.first_name," ",p.last_name) patient,p.patient_no '
        . 'FROM `' . table('appointments') . '` a JOIN `' . table('patients') . '` p ON p.id=a.patient_id '
        . 'WHERE a.id=? AND a.branch_id=? FOR UPDATE'
    );
    $stmt->execute([$appointmentId,$branchId]);
    $row = $stmt->fetch();
    if (!$row) throw new RuntimeException('Appointment not found.');
    return $row;
}

function integrity_lock_appointment_resources(PDO $pdo, int $branchId, int $patientId, int $dentistUserId, int $chairId, int $inhouseProviderUserId, string $chairName, string $startAt, string $endAt, int $excludeAppointmentId = 0): void
{
    $appointmentTable=table('appointments');
    $serviceTable=table('appointment_services');
    $segmentReady=schema_table_exists($serviceTable);
    $resourceConditions=[];$resourceArgs=[];
    if($dentistUserId>0){$resourceConditions[]='a.dentist_user_id=?';$resourceArgs[]=$dentistUserId;}
    if($inhouseProviderUserId>0){$resourceConditions[]='a.inhouse_provider_user_id=?';$resourceArgs[]=$inhouseProviderUserId;}
    if($chairId>0&&schema_column_exists($appointmentTable,'chair_id')){$resourceConditions[]='a.chair_id=?';$resourceArgs[]=$chairId;}
    elseif(trim($chairName)!==''){$resourceConditions[]='LOWER(TRIM(COALESCE(a.chair_name,"")))=LOWER(TRIM(?))';$resourceArgs[]=trim($chairName);}

    $conditions=['a.patient_id=?'];$args=[$branchId,$endAt,$startAt,$excludeAppointmentId,$patientId];
    if($resourceConditions){
        $legacyGuard=$segmentReady?'NOT EXISTS(SELECT 1 FROM `' . $serviceTable . '` aps0 WHERE aps0.appointment_id=a.id) AND ':'';
        $conditions[]='('.$legacyGuard.'('.implode(' OR ',$resourceConditions).'))';
        array_push($args,...$resourceArgs);
    }
    $sql='SELECT a.id FROM `' . $appointmentTable . '` a WHERE a.branch_id=? AND a.status NOT IN ("cancelled","no_show") '
        . 'AND a.start_at<? AND a.end_at>? AND a.id<>? AND ('.implode(' OR ',$conditions).') FOR UPDATE';
    $stmt=$pdo->prepare($sql);$stmt->execute($args);$stmt->fetchAll();

    if($segmentReady&&($dentistUserId>0||$inhouseProviderUserId>0||$chairId>0||trim($chairName)!=='')){
        $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);}
        $sql='SELECT aps.id FROM `' . $serviceTable . '` aps JOIN `' . $appointmentTable . '` a ON a.id=aps.appointment_id '
            . 'WHERE a.branch_id=? AND a.status NOT IN ("cancelled","no_show") AND aps.status IN ("planned","completed") '
            . 'AND aps.start_at<? AND aps.end_at>? AND a.id<>? AND ('.implode(' OR ',$conditions).') FOR UPDATE';
        $stmt=$pdo->prepare($sql);$stmt->execute($args);$stmt->fetchAll();
    }
}

function integrity_active_queue_for_appointment(PDO $pdo, int $appointmentId, bool $lock = true): array
{
    $sql = 'SELECT * FROM `' . table('queue_entries') . '` WHERE appointment_id=? AND status NOT IN ("completed","cancelled") ORDER BY id DESC';
    if ($lock) $sql .= ' FOR UPDATE';
    $stmt = $pdo->prepare($sql);
    $stmt->execute([$appointmentId]);
    return $stmt->fetchAll();
}

function integrity_cancel_appointment_queue(PDO $pdo, int $appointmentId, string $reason): int
{
    $rows = integrity_active_queue_for_appointment($pdo,$appointmentId,true);
    foreach ($rows as $row) {
        $old = (string) $row['status'];
        $pdo->prepare('UPDATE `' . table('queue_entries') . '` SET status="cancelled",updated_at=NOW() WHERE id=?')->execute([(int)$row['id']]);
        if (schema_table_exists(table('queue_status_history'))) {
            $pdo->prepare(
                'INSERT INTO `' . table('queue_status_history') . '` (queue_entry_id,previous_status,new_status,notes,changed_by,created_at) VALUES (?, ?,"cancelled",?,?,NOW())'
            )->execute([(int)$row['id'],$old,$reason,(int)(current_user()['id']??0)]);
        }
    }
    return count($rows);
}

function transaction_foundation_schema_ready(): bool
{
    return schema_table_exists(table('cashier_transaction_locks'))
        && schema_table_exists(table('invoice_state_history'))
        && schema_table_exists(table('transaction_audit_events'))
        && schema_table_exists(table('payment_method_rules'))
        && schema_column_exists(table('invoices'),'flow_state')
        && schema_column_exists(table('invoices'),'balance_due')
        && schema_column_exists(table('payments'),'reference_required')
        && schema_column_exists(table('cashier_sessions'),'active_invoice_id');
}

function transaction_method_requires_reference(PDO $pdo, string $method): bool
{
    $method = strtolower(trim($method));
    if ($method === '') return false;
    if (schema_table_exists(table('payment_method_rules'))) {
        $stmt = $pdo->prepare('SELECT requires_reference FROM `' . table('payment_method_rules') . '` WHERE method_code=? AND is_active=1 LIMIT 1');
        $stmt->execute([$method]);
        $value = $stmt->fetchColumn();
        if ($value !== false) return (int) $value === 1;
    }
    return in_array($method, ['card','credit_card','debit_card','gcash','maya','bank_transfer','online','digital_wallet','mixed'], true);
}

function transaction_assert_payment_reference(PDO $pdo, string $method, ?string $referenceNo): void
{
    if (transaction_method_requires_reference($pdo, $method) && trim((string) $referenceNo) === '') {
        throw new RuntimeException('Reference number is required for ' . ucwords(str_replace('_',' ',strtolower(trim($method)))) . ' payments.');
    }
}

function transaction_record_audit_event(PDO $pdo, array $event): void
{
    if (!schema_table_exists(table('transaction_audit_events'))) return;
    $stmt = $pdo->prepare(
        'INSERT INTO `' . table('transaction_audit_events') . '` '
        . '(branch_id,patient_id,invoice_id,payment_id,cashier_session_id,cashier_window_id,event_type,event_label,previous_status,new_status,amount,reason,metadata_json,created_by,created_at) '
        . 'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())'
    );
    $metadata = $event['metadata_json'] ?? null;
    if (is_array($metadata)) $metadata = json_encode($metadata, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    $stmt->execute([
        isset($event['branch_id']) ? (int) $event['branch_id'] : null,
        isset($event['patient_id']) ? (int) $event['patient_id'] : null,
        isset($event['invoice_id']) ? (int) $event['invoice_id'] : null,
        isset($event['payment_id']) ? (int) $event['payment_id'] : null,
        isset($event['cashier_session_id']) ? (int) $event['cashier_session_id'] : null,
        isset($event['cashier_window_id']) ? (int) $event['cashier_window_id'] : null,
        trim((string) ($event['event_type'] ?? 'transaction_event')),
        trim((string) ($event['event_label'] ?? 'Transaction event')),
        isset($event['previous_status']) ? (string) $event['previous_status'] : null,
        isset($event['new_status']) ? (string) $event['new_status'] : null,
        isset($event['amount']) ? (float) $event['amount'] : null,
        isset($event['reason']) ? (string) $event['reason'] : null,
        $metadata,
        isset($event['created_by']) ? (int) $event['created_by'] : (int) (current_user()['id'] ?? 0),
    ]);
}
