<?php

declare(strict_types=1);

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

if (is_file(DENTAL_ROOT . '/app/chair_resource.php')) {
    require_once DENTAL_ROOT . '/app/chair_resource.php';
}

/**
 * Appointment planning helpers for multi-service visits and grouped future visits.
 * Existing single-service appointments remain valid and are transparently treated
 * as a one-segment visit.
 */

function appointment_planning_schema_ready(): bool
{
    return schema_table_exists(table('appointment_services'))
        && schema_table_exists(table('appointment_series'))
        && schema_column_exists(table('appointments'), 'series_id')
        && schema_column_exists(table('appointments'), 'series_sequence');
}

function appointment_requested_service_ids(array $source): array
{
    $ids = [];
    $primary = (int)($source['service_id'] ?? 0);
    if ($primary > 0) $ids[] = $primary;
    foreach ((array)($source['additional_service_ids'] ?? []) as $value) {
        $id = (int)$value;
        if ($id > 0) $ids[] = $id;
    }
    foreach ((array)($source['service_ids'] ?? []) as $value) {
        $id = (int)$value;
        if ($id > 0) $ids[] = $id;
    }
    $ids = array_values(array_unique($ids));
    if (count($ids) > 12) throw new RuntimeException('A single visit can contain at most 12 planned services.');
    return $ids;
}

function appointment_service_definitions(int $branchId, array $serviceIds, ?array $allowedScopes = null): array
{
    $serviceIds = array_values(array_unique(array_filter(array_map('intval', $serviceIds), static fn(int $id): bool => $id > 0)));
    if (!$serviceIds) return [];
    if ($allowedScopes === null) $allowedScopes = current_user() ? ['public','reception'] : ['public'];
    $allowedScopes = array_values(array_unique(array_map('strval', $allowedScopes)));
    $rows = [];
    foreach ($serviceIds as $serviceId) {
        $service = service_definition($serviceId, $branchId);
        if (!$service) throw new RuntimeException('One of the selected services is no longer available. Reload the appointment form.');
        $scope = service_booking_scope($service);
        if (!in_array($scope, $allowedScopes, true)) {
            throw new RuntimeException('“' . (string)($service['name'] ?? 'Selected service') . '” cannot be booked from this appointment workflow. It must be ordered or recorded from the authorized clinical workflow.');
        }
        $rows[] = $service;
    }
    return $rows;
}

function appointment_confirmation_schema_ready(): bool
{
    return schema_column_exists(table('appointments'),'booking_source')
        && schema_column_exists(table('appointments'),'confirmation_source')
        && schema_column_exists(table('appointments'),'confirmation_note')
        && schema_column_exists(table('appointments'),'patient_agreed_at');
}

function appointment_confirmation_capture(array $source, string $status, string $defaultBookingSource = 'staff'): array
{
    $bookingSources = ['phone','in_person','email','online_request','patient_portal','internal','staff'];
    $confirmationSources = ['online_request','patient_phone','patient_in_person','patient_email','patient_portal','guardian_phone','guardian_in_person'];
    $bookingSource = trim((string)($source['booking_source'] ?? $defaultBookingSource));
    if (!in_array($bookingSource,$bookingSources,true)) $bookingSource = $defaultBookingSource;
    $confirmationSource = trim((string)($source['confirmation_source'] ?? ''));
    $confirmationNote = trim((string)($source['confirmation_note'] ?? ''));
    if ($status === 'confirmed') {
        if (!in_array($confirmationSource,$confirmationSources,true)) {
            throw new RuntimeException('Record how the patient or guardian agreed before marking the appointment confirmed. For online requests, choose Patient submitted online request.');
        }
        if (mb_strlen($confirmationNote) < 5) {
            throw new RuntimeException('Add a brief confirmation note, such as the call outcome, in-person agreement, email reply, or portal acceptance.');
        }
        return [
            'booking_source'=>$bookingSource,
            'confirmation_source'=>$confirmationSource,
            'confirmation_note'=>mb_substr($confirmationNote,0,500),
            'patient_agreed_at'=>date('Y-m-d H:i:s'),
        ];
    }
    return [
        'booking_source'=>$bookingSource,
        'confirmation_source'=>null,
        'confirmation_note'=>null,
        'patient_agreed_at'=>null,
    ];
}

function appointment_service_bundle_summary(array $services): string
{
    if (!$services) return 'Dental appointment';
    $names = array_values(array_map(static fn(array $row): string => trim((string)($row['name'] ?? 'Dental service')), $services));
    if (count($names) === 1) return $names[0];
    return $names[0] . ' +' . (count($names) - 1) . ' service' . (count($names) === 2 ? '' : 's');
}

function appointment_bundle_provider_model(array $services): string
{
    $types = array_values(array_unique(array_map('service_provider_type', $services)));
    if (count($types) === 1) return $types[0];
    return 'mixed';
}

function appointment_bundle_duration_minutes(array $segments): int
{
    if (!$segments) return 0;
    $first = strtotime((string)$segments[0]['start_at']);
    $last = strtotime((string)$segments[count($segments) - 1]['end_at']);
    return max(0, (int)round(($last - $first) / 60));
}


function appointment_segment_within_published_availability(int $branchId, array $service, string $startAt, string $endAt, int $dentistUserId = 0): bool
{
    $date=date('Y-m-d',strtotime($startAt));
    if($date<date('Y-m-d')||date('Y-m-d',strtotime($endAt))!==$date)return false;
    $prep=max(0,(int)($service['preparation_minutes']??0));
    $cleanup=max(0,(int)($service['cleanup_minutes']??0));
    $reserveStart=time_minutes(date('H:i',strtotime($startAt)))-$prep;
    $reserveEnd=time_minutes(date('H:i',strtotime($endAt)))+$cleanup;
    if(service_provider_type($service)==='dentist'){
        if($dentistUserId<1)return false;
        foreach(doctor_intervals_for_date($dentistUserId,$branchId,$date) as [$availableStart,$availableEnd]){
            if($reserveStart>=$availableStart&&$reserveEnd<=$availableEnd)return true;
        }
        return false;
    }
    $hours=branch_hours_for_date($branchId,$date);
    if(!$hours)return false;
    return $reserveStart>=time_minutes((string)$hours['open_time'])&&$reserveEnd<=time_minutes((string)$hours['close_time']);
}

function appointment_build_segments(
    int $branchId,
    int $patientId,
    array $services,
    string $date,
    string $time,
    int $dentistUserId,
    int $excludeAppointmentId = 0,
    bool $enforcePublishedAvailability = true,
    int $requestedInhouseProviderId = 0,
    int $requestedChairId = 0
): array {
    if (!$services) throw new RuntimeException('Select at least one service.');
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) || !preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/', substr($time, 0, 5))) {
        throw new RuntimeException('Select a valid appointment date and time.');
    }

    $cursor = $date . ' ' . substr($time, 0, 5) . ':00';
    $segments = [];
    $dentist = $dentistUserId > 0 ? dentist_profile($dentistUserId) : null;
    $dentistName = trim((string)($dentist['display_name'] ?? $dentist['name'] ?? ''));

    foreach ($services as $index => $service) {
        $providerType = service_provider_type($service);
        $prep = max(0, (int)($service['preparation_minutes'] ?? 0));
        $cleanup = max(0, (int)($service['cleanup_minutes'] ?? 0));
        if ($index > 0) {
            $previousCleanup = max(0, (int)($services[$index - 1]['cleanup_minutes'] ?? 0));
            $cursor = date('Y-m-d H:i:s', strtotime($cursor . ' +' . ($previousCleanup + $prep) . ' minutes'));
        }
        $startAt = $cursor;
        $duration = max(5, (int)($service['duration_minutes'] ?? 30));
        $endAt = date('Y-m-d H:i:s', strtotime($startAt . ' +' . $duration . ' minutes'));
        $segmentTime = date('H:i', strtotime($startAt));
        $segmentDate = date('Y-m-d', strtotime($startAt));
        $chairId = 0;
        $chairName = '';
        $inhouseProviderId = 0;
        $inhouseProviderName = '';
        $providerName = '';

        if ($providerType === 'dentist') {
            if ($dentistUserId < 1) throw new RuntimeException('Select a Dentist because one or more services require a Dentist.');
            $readinessReason = null;
            if (!dentist_service_readiness($dentistUserId, $service, $readinessReason)) {
                throw new RuntimeException('The selected Dentist cannot provide ' . (string)$service['name'] . ': ' . ($readinessReason ?: 'required credentials or clinical privilege are incomplete.'));
            }
            $binding = function_exists('chair_resource_resolve_for_doctor')
                ? chair_resource_resolve_for_doctor($dentistUserId, $branchId, $startAt, $endAt)
                : doctor_chair_binding($dentistUserId, $branchId);
            if (empty($binding['valid'])) throw new RuntimeException((string)($binding['issue'] ?? 'The selected Dentist has no valid operatory or chair assignment.'));
            $chairId = (int)($binding['chair_id'] ?? 0);
            $chairName = (string)($binding['chair_name'] ?? '');
            $providerName = $dentistName !== '' ? $dentistName : 'Dentist';
            if ($enforcePublishedAvailability && !appointment_segment_within_published_availability($branchId,$service,$startAt,$endAt,$dentistUserId)) {
                throw new RuntimeException((string)$service['name'] . ' cannot start at ' . date('g:i A', strtotime($startAt)) . ' within the selected Dentist’s published availability.');
            }
        } else {
            if (!inhouse_resource_schema_ready()) throw new RuntimeException('Apply Version 0.5.8.4 before booking an in-house service.');
            if ($enforcePublishedAvailability && !appointment_segment_within_published_availability($branchId,$service,$startAt,$endAt,0)) {
                throw new RuntimeException((string)$service['name'] . ' has no complete in-house staff-and-chair slot at ' . date('g:i A', strtotime($startAt)) . '.');
            }
            $assignment = inhouse_slot_assignment(
                $branchId,
                $segmentDate,
                $segmentTime,
                $service,
                $excludeAppointmentId,
                $requestedInhouseProviderId,
                $requestedChairId
            );
            if (empty($assignment['valid'])) throw new RuntimeException((string)($assignment['issue'] ?? 'No in-house resource is available for this service.'));
            $inhouseProviderId = (int)$assignment['provider_id'];
            $inhouseProviderName = (string)$assignment['provider_name'];
            $providerName = $inhouseProviderName;
            $chairId = (int)$assignment['chair_id'];
            $chairName = (string)$assignment['chair_name'];
        }

        $conflicts = appointment_resource_conflicts(
            $branchId,
            $patientId,
            $providerType === 'dentist' ? $dentistUserId : 0,
            $chairName,
            $startAt,
            $endAt,
            $excludeAppointmentId,
            $prep,
            $cleanup,
            $inhouseProviderId,
            $chairId
        );
        if (!empty($conflicts['has_conflict'])) {
            throw new RuntimeException((string)$service['name'] . ': ' . appointment_conflict_message($conflicts));
        }

        $segments[] = [
            'sequence_no' => $index + 1,
            'service_id' => (int)$service['id'],
            'service_name' => (string)$service['name'],
            'provider_type' => $providerType,
            'required_privilege_code' => service_required_privilege_code($service),
            'duration_minutes' => $duration,
            'preparation_minutes' => $prep,
            'cleanup_minutes' => $cleanup,
            'dentist_user_id' => $providerType === 'dentist' ? $dentistUserId : null,
            'dentist_name' => $providerType === 'dentist' ? $providerName : null,
            'inhouse_provider_user_id' => $providerType === 'inhouse' ? $inhouseProviderId : null,
            'inhouse_provider_name' => $providerType === 'inhouse' ? $inhouseProviderName : null,
            'chair_id' => $chairId ?: null,
            'chair_name' => $chairName !== '' ? $chairName : null,
            'start_at' => $startAt,
            'end_at' => $endAt,
            'status' => 'planned',
        ];
        $cursor = $endAt;
    }

    return $segments;
}

function appointment_store_segments(PDO $pdo, int $appointmentId, array $segments): void
{
    if (!appointment_planning_schema_ready()) return;
    $state=$pdo->prepare('SELECT status FROM `' . table('appointments') . '` WHERE id=? LIMIT 1');
    $state->execute([$appointmentId]);
    $appointmentStatus=(string)($state->fetchColumn()?:'');
    $existingCountStmt=$pdo->prepare('SELECT COUNT(*) FROM `' . table('appointment_services') . '` WHERE appointment_id=?');
    $existingCountStmt->execute([$appointmentId]);
    $existingCount=(int)$existingCountStmt->fetchColumn();
    if($existingCount>0&&!in_array($appointmentStatus,['requested','pending_confirmation','confirmed'],true)){
        throw new RuntimeException('Planned services cannot be removed or rewritten after check-in. Use Defer, Move, Follow-up, or Not performed so the original visit history is preserved.');
    }
    $pdo->prepare('DELETE FROM `' . table('appointment_services') . '` WHERE appointment_id=?')->execute([$appointmentId]);
    $insert = $pdo->prepare(
        'INSERT INTO `' . table('appointment_services') . '` '
        . '(appointment_id,service_id,sequence_no,service_name,provider_type,required_privilege_code,duration_minutes,preparation_minutes,cleanup_minutes,dentist_user_id,dentist_name,inhouse_provider_user_id,inhouse_provider_name,chair_id,chair_name,start_at,end_at,status,created_at) '
        . 'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,? ,NOW())'
    );
    foreach ($segments as $segment) {
        $insert->execute([
            $appointmentId,
            (int)$segment['service_id'],
            (int)$segment['sequence_no'],
            (string)$segment['service_name'],
            (string)$segment['provider_type'],
            $segment['required_privilege_code'] ?: null,
            (int)$segment['duration_minutes'],
            (int)$segment['preparation_minutes'],
            (int)$segment['cleanup_minutes'],
            $segment['dentist_user_id'] ?: null,
            $segment['dentist_name'] ?: null,
            $segment['inhouse_provider_user_id'] ?: null,
            $segment['inhouse_provider_name'] ?: null,
            $segment['chair_id'] ?: null,
            $segment['chair_name'] ?: null,
            (string)$segment['start_at'],
            (string)$segment['end_at'],
            (string)($segment['status'] ?? 'planned'),
        ]);
    }
}

function appointment_services_for_ids(PDO $pdo, array $appointmentIds): array
{
    $appointmentIds = array_values(array_unique(array_filter(array_map('intval', $appointmentIds), static fn(int $id): bool => $id > 0)));
    if (!$appointmentIds || !appointment_planning_schema_ready()) return [];
    $placeholders = implode(',', array_fill(0, count($appointmentIds), '?'));
    $stmt = $pdo->prepare('SELECT * FROM `' . table('appointment_services') . '` WHERE appointment_id IN (' . $placeholders . ') ORDER BY appointment_id,sequence_no,id');
    $stmt->execute($appointmentIds);
    $grouped = [];
    foreach ($stmt->fetchAll() as $row) $grouped[(int)$row['appointment_id']][] = $row;
    return $grouped;
}

function appointment_create_series(PDO $pdo, int $branchId, int $patientId, string $name, int $createdBy): int
{
    if (!appointment_planning_schema_ready()) return 0;
    $name = trim($name);
    if ($name === '') $name = 'Future appointment series';
    $pdo->prepare('INSERT INTO `' . table('appointment_series') . '` (branch_id,patient_id,series_name,status,created_by,created_at) VALUES (?,?,? ,"active",?,NOW())')
        ->execute([$branchId,$patientId,mb_substr($name,0,190),$createdBy ?: null]);
    return (int)$pdo->lastInsertId();
}

function appointment_repeat_dates(string $date, int $count, int $intervalWeeks): array
{
    $count = max(1, min(12, $count));
    $intervalWeeks = max(1, min(52, $intervalWeeks));
    $dates = [];
    for ($index = 0; $index < $count; $index++) {
        $dates[] = date('Y-m-d', strtotime($date . ' +' . ($index * $intervalWeeks) . ' weeks'));
    }
    return $dates;
}

function appointment_bundle_dentists(int $branchId, array $services, int $requestedDentistId = 0, bool $newPatient = false): array
{
    $dentistServices = array_values(array_filter($services, static fn(array $service): bool => service_provider_type($service) === 'dentist'));
    if (!$dentistServices) return [['id'=>0,'name'=>'In-house clinic team','specialty_name'=>'']];
    $candidates = bookable_dentists_for_service($branchId, (int)$dentistServices[0]['id'], $newPatient);
    $result = [];
    foreach ($candidates as $doctor) {
        $id = (int)($doctor['id'] ?? 0);
        if ($id < 1 || ($requestedDentistId > 0 && $id !== $requestedDentistId)) continue;
        $ready = true;
        foreach ($dentistServices as $service) {
            $reason = null;
            if (!dentist_service_readiness($id, $service, $reason)) { $ready = false; break; }
        }
        if (!$ready) continue;
        // Do not reject a dentist only because there is no chair bound right now.
        // Appointment search can run for a future date where a chair session exists.
        // The exact chair is resolved later per candidate start/end time.
        $binding = doctor_chair_binding($id,$branchId);
        $doctor['chair_name'] = !empty($binding['valid']) ? (string)($binding['chair_name'] ?? '') : '';
        $doctor['chair_id'] = !empty($binding['valid']) ? (int)($binding['chair_id'] ?? 0) : 0;
        $result[] = $doctor;
    }
    return $result;
}

function appointment_bundle_available_options(
    int $branchId,
    string $date,
    array $services,
    int $requestedDentistId = 0,
    bool $newPatient = false,
    int $excludeAppointmentId = 0,
    int $patientId = 0
): array {
    if (!$services) return [];
    $primary = $services[0];
    $dentists = appointment_bundle_dentists($branchId,$services,$requestedDentistId,$newPatient);
    if (!$dentists) return [];
    $options = [];
    foreach ($dentists as $doctor) {
        $doctorId = (int)($doctor['id'] ?? 0);
        $baseSlots = service_available_slots(
            $branchId,
            $primary,
            $date,
            service_provider_type($primary) === 'dentist' ? $doctorId : 0,
            $excludeAppointmentId
        );
        foreach ($baseSlots as $time) {
            try {
                $segments = appointment_build_segments($branchId,$patientId,$services,$date,$time,$doctorId,$excludeAppointmentId,true);
                $doctorChair = trim((string)($doctor['chair_name'] ?? ''));
                $segmentChair = trim((string)($segments[0]['chair_name'] ?? ''));
                $options[] = [
                    'time'=>$time,
                    'label'=>date('g:i A',strtotime($time)),
                    'doctor_id'=>$doctorId,
                    'doctor_name'=>(string)($doctor['name'] ?? 'In-house clinic team'),
                    'specialty_name'=>(string)($doctor['specialty_name'] ?? ''),
                    'chair_name'=>$doctorChair !== '' ? $doctorChair : $segmentChair,
                    'duration_minutes'=>appointment_bundle_duration_minutes($segments),
                    'end_time'=>date('H:i',strtotime((string)$segments[count($segments)-1]['end_at'])),
                    'segments'=>$segments,
                ];
            } catch (Throwable) {
                // The primary slot is not a complete multi-service resource slot.
            }
        }
    }
    usort($options,static fn(array $a,array $b): int => [$a['time'],$a['doctor_name']] <=> [$b['time'],$b['doctor_name']]);
    $unique=[];$result=[];
    foreach($options as $option){$key=$option['time'].'|'.$option['doctor_id'];if(isset($unique[$key]))continue;$unique[$key]=true;$result[]=$option;}
    return $result;
}

function appointment_service_disposition_schema_ready(): bool
{
    return appointment_planning_schema_ready()
        && schema_table_exists(table('appointment_service_dispositions'))
        && schema_column_exists(table('appointment_services'),'disposition_reason')
        && schema_column_exists(table('appointment_services'),'destination_appointment_id')
        && schema_column_exists(table('appointment_services'),'source_appointment_service_id');
}

function appointment_service_disposition_context(PDO $pdo, int $appointmentServiceId, bool $forUpdate = false): array
{
    $lock=$forUpdate?' FOR UPDATE':'';
    $stmt=$pdo->prepare(
        'SELECT aps.*,a.branch_id,a.patient_id,a.status appointment_status,a.start_at appointment_start_at,a.dentist_user_id appointment_dentist_user_id,'
        . 'a.service_name appointment_service_name,a.appointment_origin '
        . 'FROM `' . table('appointment_services') . '` aps '
        . 'JOIN `' . table('appointments') . '` a ON a.id=aps.appointment_id '
        . 'WHERE aps.id=? LIMIT 1'.$lock
    );
    $stmt->execute([$appointmentServiceId]);
    $row=$stmt->fetch();
    if(!$row)throw new RuntimeException('The planned service could not be found.');
    if(!integrity_branch_scope_allowed((int)$row['branch_id'],null))throw new RuntimeException('This planned service belongs to another clinic branch.');
    return $row;
}

function appointment_append_service_to_future_appointment(PDO $pdo, array $sourceSegment, int $destinationAppointmentId): int
{
    $destinationStmt=$pdo->prepare('SELECT * FROM `' . table('appointments') . '` WHERE id=? AND branch_id=? AND patient_id=? LIMIT 1 FOR UPDATE');
    $destinationStmt->execute([$destinationAppointmentId,(int)$sourceSegment['branch_id'],(int)$sourceSegment['patient_id']]);
    $destination=$destinationStmt->fetch();
    if(!$destination)throw new RuntimeException('The selected future appointment does not belong to this patient or branch.');
    if(!in_array((string)$destination['status'],['requested','pending_confirmation','confirmed'],true))throw new RuntimeException('Services can be moved only to a future requested, pending-confirmation, or confirmed appointment.');
    if(strtotime((string)$destination['start_at'])<=time())throw new RuntimeException('The destination appointment must be in the future.');
    if((int)$destination['id']===(int)$sourceSegment['appointment_id'])throw new RuntimeException('Choose a different future appointment.');

    $existingSource=$pdo->prepare('SELECT id FROM `' . table('appointment_services') . '` WHERE appointment_id=? AND source_appointment_service_id=? LIMIT 1');
    $existingSource->execute([$destinationAppointmentId,(int)$sourceSegment['id']]);
    if($existingSource->fetchColumn())throw new RuntimeException('This service has already been moved to the selected appointment.');

    $segmentStmt=$pdo->prepare('SELECT * FROM `' . table('appointment_services') . '` WHERE appointment_id=? AND status="planned" ORDER BY sequence_no,id');
    $segmentStmt->execute([$destinationAppointmentId]);
    $destinationSegments=$segmentStmt->fetchAll();
    $preservedSourceLinks=[];
    foreach($destinationSegments as $destinationSegment){
        $sourceLink=(int)($destinationSegment['source_appointment_service_id']??0);
        if($sourceLink>0)$preservedSourceLinks[(int)$destinationSegment['service_id']]=$sourceLink;
    }
    $serviceIds=array_map(static fn(array $row):int=>(int)$row['service_id'],$destinationSegments);
    if(!$serviceIds)$serviceIds=[(int)$destination['service_id']];
    if(in_array((int)$sourceSegment['service_id'],$serviceIds,true))throw new RuntimeException('The destination appointment already contains this service.');
    $serviceIds[]=(int)$sourceSegment['service_id'];
    $services=appointment_service_definitions((int)$destination['branch_id'],$serviceIds,['public','reception','dentist_order','clinical_intake','inhouse_only']);

    $sourceDefinition=service_definition((int)$sourceSegment['service_id'],(int)$destination['branch_id']);
    if(!$sourceDefinition)throw new RuntimeException('The service is no longer active and cannot be moved.');
    $sourceDentistId=(int)($sourceSegment['dentist_user_id']??0);
    $destinationDentistId=(int)($destination['dentist_user_id']??0);
    if(service_provider_type($sourceDefinition)==='dentist'){
        if($sourceDentistId>0&&$destinationDentistId>0&&$sourceDentistId!==$destinationDentistId){
            throw new RuntimeException('For continuity of care, move this service to an appointment with the same Dentist or create a new follow-up.');
        }
        if($destinationDentistId<1)$destinationDentistId=$sourceDentistId;
        if($destinationDentistId<1)throw new RuntimeException('A Dentist must be assigned before this service can be moved.');
    }

    $date=date('Y-m-d',strtotime((string)$destination['start_at']));
    $time=date('H:i',strtotime((string)$destination['start_at']));
    $segments=appointment_build_segments(
        (int)$destination['branch_id'],
        (int)$destination['patient_id'],
        $services,
        $date,
        $time,
        $destinationDentistId,
        (int)$destination['id'],
        true,
        (int)($destination['inhouse_provider_user_id']??0),
        (int)($destination['chair_id']??0)
    );
    foreach($segments as $segment){
        integrity_lock_appointment_resources($pdo,(int)$destination['branch_id'],(int)$destination['patient_id'],(int)($segment['dentist_user_id']??0),(int)($segment['chair_id']??0),(int)($segment['inhouse_provider_user_id']??0),(string)($segment['chair_name']??''),(string)$segment['start_at'],(string)$segment['end_at'],(int)$destination['id']);
    }
    appointment_store_segments($pdo,(int)$destination['id'],$segments);
    $first=$segments[0];
    $last=$segments[count($segments)-1];
    $firstInhouse=null;foreach($segments as $candidate)if((string)$candidate['provider_type']==='inhouse'){$firstInhouse=$candidate;break;}
    $dentistRequired=false;foreach($services as $service)if(service_provider_type($service)==='dentist'){$dentistRequired=true;break;}
    $summary=appointment_service_bundle_summary($services);
    $pdo->prepare('UPDATE `' . table('appointments') . '` SET service_id=?,service_name=?,provider_type=?,dentist_user_id=?,dentist_name=?,inhouse_provider_user_id=?,inhouse_provider_name=?,end_at=?,chair_id=?,chair_name=?,updated_at=NOW() WHERE id=?')
        ->execute([
            (int)$services[0]['id'],$summary,(string)$first['provider_type'],$dentistRequired?$destinationDentistId:null,$dentistRequired?(trim((string)($first['dentist_name']??''))!==''?(string)$first['dentist_name']:(string)($destination['dentist_name']??'')):null,
            (int)($firstInhouse['inhouse_provider_user_id']??0)?:null,(string)($firstInhouse['inhouse_provider_name']??'')?:null,(string)$last['end_at'],(int)($first['chair_id']??0)?:null,(string)($first['chair_name']??'')?:null,(int)$destination['id'],
        ]);
    $link=$pdo->prepare('UPDATE `' . table('appointment_services') . '` SET source_appointment_service_id=? WHERE appointment_id=? AND service_id=? LIMIT 1');
    foreach($preservedSourceLinks as $serviceId=>$sourceAppointmentServiceId){
        $link->execute([$sourceAppointmentServiceId,(int)$destination['id'],$serviceId]);
    }
    $link->execute([(int)$sourceSegment['id'],(int)$destination['id'],(int)$sourceSegment['service_id']]);
    return (int)$destination['id'];
}

function appointment_create_service_followup(PDO $pdo, array $sourceSegment, string $reason): int
{
    $branchId=(int)$sourceSegment['branch_id'];
    $patientId=(int)$sourceSegment['patient_id'];
    $service=service_definition((int)$sourceSegment['service_id'],$branchId);
    if(!$service)throw new RuntimeException('The service is no longer active and cannot be scheduled as a follow-up.');
    $doctorId=service_provider_type($service)==='dentist'?(int)($sourceSegment['dentist_user_id']??$sourceSegment['appointment_dentist_user_id']??0):0;
    if(service_provider_type($service)==='dentist'&&$doctorId<1)throw new RuntimeException('The original Dentist could not be identified. Assign the follow-up manually.');
    $selected=null;
    for($offset=1;$offset<=90&&!$selected;$offset++){
        $date=date('Y-m-d',strtotime('+'.$offset.' days'));
        if(!branch_hours_for_date($branchId,$date))continue;
        $options=appointment_bundle_available_options($branchId,$date,[$service],$doctorId,false,0,$patientId);
        if($options)$selected=$options[0];
    }
    if(!$selected)throw new RuntimeException('No valid follow-up slot was found for the same Dentist within the next 90 days. Use Reception scheduling to choose a later date.');
    $segments=(array)$selected['segments'];
    foreach($segments as $segment){
        integrity_lock_appointment_resources($pdo,$branchId,$patientId,(int)($segment['dentist_user_id']??0),(int)($segment['chair_id']??0),(int)($segment['inhouse_provider_user_id']??0),(string)($segment['chair_name']??''),(string)$segment['start_at'],(string)$segment['end_at'],0);
    }
    $first=$segments[0];$last=$segments[count($segments)-1];
    $providerType=(string)$first['provider_type'];
    $notes='Follow-up created from appointment #'.(int)$sourceSegment['appointment_id'].'. Deferred service: '.(string)$sourceSegment['service_name'].'. Reason: '.$reason;
    $stmt=$pdo->prepare('INSERT INTO `' . table('appointments') . '` (branch_id,patient_id,service_id,dentist_user_id,dentist_name,inhouse_provider_user_id,inhouse_provider_name,service_name,provider_type,start_at,end_at,chair_id,chair_name,status,appointment_origin,booking_source,reason_for_visit,confirmation_status,confirmation_source,confirmation_note,patient_agreed_at,notes,created_by,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,"pending_confirmation","manual","internal",?,"pending_confirmation",NULL,NULL,NULL,?,?,NOW())');
    $stmt->execute([
        $branchId,$patientId,(int)$service['id'],(int)($first['dentist_user_id']??0)?:null,(string)($first['dentist_name']??'')?:null,(int)($first['inhouse_provider_user_id']??0)?:null,(string)($first['inhouse_provider_name']??'')?:null,(string)$service['name'],$providerType,(string)$first['start_at'],(string)$last['end_at'],(int)($first['chair_id']??0)?:null,(string)($first['chair_name']??'')?:null,'Deferred service follow-up: '.$reason,$notes,(int)(current_user()['id']??0),
    ]);
    $appointmentId=(int)$pdo->lastInsertId();
    appointment_store_segments($pdo,$appointmentId,$segments);
    $pdo->prepare('UPDATE `' . table('appointment_services') . '` SET source_appointment_service_id=? WHERE appointment_id=? ORDER BY sequence_no,id LIMIT 1')->execute([(int)$sourceSegment['id'],$appointmentId]);
    $pdo->prepare('INSERT INTO `' . table('appointment_status_history') . '` (appointment_id,previous_status,new_status,reason,changed_by,created_at) VALUES (?,NULL,"pending_confirmation",?,?,NOW())')->execute([$appointmentId,'Created for deferred service from appointment #'.(int)$sourceSegment['appointment_id'],(int)(current_user()['id']??0)]);
    workflow_event($patientId,'appointment_followup_created','Deferred service follow-up created',['source_appointment_id'=>(int)$sourceSegment['appointment_id'],'appointment_id'=>$appointmentId],date('M j, Y g:i A',strtotime((string)$first['start_at'])).' · '.(string)$service['name']);
    return $appointmentId;
}

function appointment_apply_service_disposition(PDO $pdo, int $appointmentServiceId, string $action, string $reason, int $requestedDestinationId = 0): int
{
    if(!appointment_service_disposition_schema_ready())throw new RuntimeException('Apply Version 0.5.9.8.32 before deferring or moving a planned service.');
    $allowed=['defer_next','move_existing','create_followup','not_performed'];
    if(!in_array($action,$allowed,true))throw new RuntimeException('Select a valid service disposition.');
    $reason=trim($reason);
    if(mb_strlen($reason)<5)throw new RuntimeException('Enter a clear reason for the service disposition.');
    $segment=appointment_service_disposition_context($pdo,$appointmentServiceId,true);
    if((string)$segment['status']!=='planned')throw new RuntimeException('This service already has a completed disposition and cannot be moved again.');
    $sourceStatus=(string)$segment['appointment_status'];
    $activeQueue=integrity_active_queue_for_appointment($pdo,(int)$segment['appointment_id'],true);
    if(in_array($sourceStatus,['requested','pending_confirmation','confirmed'],true)&&!$activeQueue){
        throw new RuntimeException('Before check-in, reschedule the appointment instead. Service disposition is reserved for an actual visit so history is not deleted.');
    }
    $destinationId=0;
    if($action==='not_performed'){
        $newStatus='not_performed';
    }elseif($action==='move_existing'){
        if($requestedDestinationId<1)throw new RuntimeException('Choose the future appointment that should receive this service.');
        $destinationId=appointment_append_service_to_future_appointment($pdo,$segment,$requestedDestinationId);
        $newStatus='moved';
    }elseif($action==='defer_next'){
        $candidateStmt=$pdo->prepare('SELECT id FROM `' . table('appointments') . '` WHERE branch_id=? AND patient_id=? AND id<>? AND start_at>NOW() AND status IN ("requested","pending_confirmation","confirmed") ORDER BY CASE WHEN dentist_user_id=? THEN 0 ELSE 1 END,start_at,id LIMIT 20 FOR UPDATE');
        $candidateStmt->execute([(int)$segment['branch_id'],(int)$segment['patient_id'],(int)$segment['appointment_id'],(int)($segment['dentist_user_id']??0)]);
        $lastError=null;
        foreach($candidateStmt->fetchAll(PDO::FETCH_COLUMN) as $candidateId){
            $pdo->exec('SAVEPOINT appointment_disposition_candidate');
            try{
                $destinationId=appointment_append_service_to_future_appointment($pdo,$segment,(int)$candidateId);
                $pdo->exec('RELEASE SAVEPOINT appointment_disposition_candidate');
                break;
            }catch(Throwable $exception){
                $lastError=$exception;
                $pdo->exec('ROLLBACK TO SAVEPOINT appointment_disposition_candidate');
                $pdo->exec('RELEASE SAVEPOINT appointment_disposition_candidate');
            }
        }
        if($destinationId<1)$destinationId=appointment_create_service_followup($pdo,$segment,$reason);
        $newStatus='deferred';
    }else{
        $destinationId=appointment_create_service_followup($pdo,$segment,$reason);
        $newStatus='deferred';
    }
    $pdo->prepare('UPDATE `' . table('appointment_services') . '` SET status=?,disposition_reason=?,destination_appointment_id=?,disposition_by=?,disposition_at=NOW() WHERE id=? AND status="planned"')
        ->execute([$newStatus,$reason,$destinationId?:null,(int)(current_user()['id']??0),$appointmentServiceId]);
    $pdo->prepare('INSERT INTO `' . table('appointment_service_dispositions') . '` (appointment_service_id,source_appointment_id,destination_appointment_id,action,reason,performed_by,created_at) VALUES (?,?,?,?,?,?,NOW())')
        ->execute([$appointmentServiceId,(int)$segment['appointment_id'],$destinationId?:null,$action,$reason,(int)(current_user()['id']??0)]);
    audit('appointment_service_disposition','appointment_service',$appointmentServiceId,['action'=>$action,'reason'=>$reason,'source_appointment_id'=>(int)$segment['appointment_id'],'destination_appointment_id'=>$destinationId?:null]);
    workflow_event((int)$segment['patient_id'],'appointment_service_'.$newStatus,ucwords(str_replace('_',' ',$newStatus)).' planned service',['appointment_id'=>(int)$segment['appointment_id'],'appointment_service_id'=>$appointmentServiceId,'destination_appointment_id'=>$destinationId?:null],(string)$segment['service_name'].' · '.$reason);
    return $destinationId;
}
