<?php
declare(strict_types=1);

namespace App\Controller;

use Cake\Event\EventInterface;
use Cake\Http\Response;
use Cake\I18n\FrozenTime;
use Cake\Log\Log;
use Cake\ORM\Table;
use Throwable;

class ParticipantsController extends AppController
{
    /**
     * @var \Cake\ORM\Table
     */
    protected Table $Participants;
    /**
     * @var \Cake\ORM\Table
     */
    protected Table $Login;
    /**
     * @var \Cake\ORM\Table
     */
    protected Table $CalendarEvents;
    /**
     * @var \Cake\ORM\Table
     */
    protected Table $ParticipantAddresses;
    /**
     * @var \Cake\ORM\Table
     */
    protected Table $ParticipantFiles;

    /**
     * @throws \Exception
     */
    public function initialize(): void
    {
        parent::initialize();
        $this->loadComponent('Authentication.Authentication');
        $this->loadComponent('Flash');

        $this->Participants         = $this->fetchTable('Participants');
        $this->Login                = $this->fetchTable('Login');
        $this->CalendarEvents       = $this->fetchTable('CalendarEvents');
        $this->ParticipantAddresses = $this->fetchTable('ParticipantAddresses');
        $this->ParticipantFiles     = $this->fetchTable('ParticipantFiles');

        $this->viewBuilder()->setLayout('default');
    }

    public function beforeFilter(EventInterface $event)
    {
        parent::beforeFilter($event);

        if ($this->components()->has('Authentication')) {
            $result = $this->Authentication->getResult();
            if (!$result || !$result->isValid()) {
                return $this->redirect([
                    'controller' => 'Login',
                    'action'     => 'index',
                    '?'          => ['redirect' => $this->request->getRequestTarget()],
                ]);
            }
        }
    }

    /**
     * Join + filter a Participants query to the current clinic via participants_clinics.
     */
    private function scopeParticipantsToClinic(\Cake\ORM\Query $q, int $clinicId): \Cake\ORM\Query
    {
        // INNER JOIN participants_clinics PC ON PC.participant_id = Participants.id AND PC.clinic_id = :clinicId
        return $q->join([
            'PC' => [
                'table' => 'participants_clinics',
                'type'  => 'INNER',
                'conditions' => 'PC.participant_id = Participants.id',
            ],
        ])
            ->where(['PC.clinic_id' => $clinicId])
            ->distinct(['Participants.id']);
    }

    /**
     * A list of clinicians limited to the current clinic (used for filters/selects).
     */
    private function cliniciansForClinic(int $clinicId): array
    {
        $q = $this->Login->find()
            ->select(['Login.id','Login.first_name','Login.last_name','Login.email'])
            ->join([
                'CU' => [
                    'table' => 'clinic_users',
                    'type'  => 'INNER',
                    'conditions' => 'CU.login_id = Login.id',
                ],
            ])
            ->where(['CU.clinic_id' => $clinicId])
            ->orderAsc('Login.first_name');

        $out = [];
        foreach ($q->all() as $u) {
            $name = trim(($u->first_name ?? '') . ' ' . ($u->last_name ?? ''));
            $out[(int)$u->id] = $name !== '' ? $name : (string)$u->email;
        }

        return $out;
    }

    /**
     * After saving a participant, ensure it is linked to the current clinic in participants_clinics.
     */
    private function ensureParticipantLinkedToClinic(int $participantId, int $clinicId): void
    {
        $conn = $this->Participants->getConnection();
        // Insert if not exists (portable version)
        $sql = 'INSERT INTO participants_clinics (participant_id, clinic_id)
                SELECT :pid, :cid FROM DUAL
                WHERE NOT EXISTS (
                    SELECT 1 FROM participants_clinics
                    WHERE participant_id = :pid AND clinic_id = :cid
                )';
        try {
            $conn->execute($sql, ['pid' => $participantId, 'cid' => $clinicId]);
        } catch (\Throwable $e) {
            // If the DB doesn’t support SELECT FROM DUAL, try a simple insert with ignore semantics where available
            try {
                $conn->insert('participants_clinics', [
                    'participant_id' => $participantId,
                    'clinic_id'      => $clinicId,
                ]);
            } catch (\Throwable $ignored) {}
        }
    }

    public function index()
    {
        $q       = (string)$this->request->getQuery('q', '');
        $perPage = max(1, (int)$this->request->getQuery('limit', 20));
        $cid     = (string)$this->request->getQuery('cid', '');
        $clinicId = (int)$this->currentClinicId();

        // Base query
        $query = $this->Participants->find()
            ->contain([
                'Login',
                'Contacts' => function ($q) {
                    return $q->select(['id','participant_id','type','label','value','is_primary','sort'])
                        ->orderAsc('sort');
                },
                'ParticipantAddresses' => function ($q) {
                    return $q->select([
                        'id','participant_id','address_type','address_line1','address_line2',
                        'city','region','postcode','country'
                    ]);
                },
            ])
            ->orderAsc('Participants.created');

        // Scope to clinic
        $query = $this->scopeParticipantsToClinic($query, $clinicId);

        // Search
        if ($q !== '') {
            $query->where([
                'OR' => [
                    'Participants.first_name LIKE'     => "%{$q}%",
                    'Participants.middle_name LIKE'    => "%{$q}%",
                    'Participants.last_name LIKE'      => "%{$q}%",
                    'Participants.preferred_name LIKE' => "%{$q}%",
                    'Participants.email LIKE'          => "%{$q}%",
                    'Participants.phone LIKE'          => "%{$q}%",
                ],
            ]);
        }

        // Filter by clinician (also ensures clinician belongs to this clinic)
        if ($cid !== '') {
            $query
                ->matching('Login', function ($q2) use ($cid, $clinicId) {
                    // Join clinic_users to ensure the clinician is from this clinic
                    return $q2->join([
                        'CU' => [
                            'table' => 'clinic_users',
                            'type'  => 'INNER',
                            'conditions' => 'CU.login_id = Login.id',
                        ],
                    ])
                        ->where(['Login.id' => (int)$cid, 'CU.clinic_id' => $clinicId]);
                })
                ->distinct(['Participants.id']);
        }

        $this->paginate = [
            'limit' => $perPage,
            'order' => ['Participants.created' => 'ASC'],
        ];
        $participants = $this->paginate($query);

        $clinicians = $this->cliniciansForClinic($clinicId);

        $this->set(compact('participants', 'q', 'perPage', 'clinicians', 'cid'));
    }

    public function view(int $id)
    {
        $this->request->allowMethod(['get']);
        $clinicId = (int)$this->currentClinicId();

        $participant = $this->scopeParticipantsToClinic(
            $this->Participants->find()->where(['Participants.id' => $id])->contain(['Login']),
            $clinicId
        )
            ->firstOrFail();

        $this->set(compact('participant'));

        $contacts = $this->ParticipantContacts
            ->find()
            ->where(['participant_id' => $id])
            ->orderAsc('sort')
            ->all();
        $this->set(compact('contacts'));

    }

    public function add()
    {
        $clinicId = (int)$this->currentClinicId();

        if ($this->request->getParam('_ext') === 'json') {
            // JSON (API) create
            $this->request->allowMethod(['post']);
            $data = json_decode((string)$this->request->getBody(), true) ?? [];
            $data = $this->normalizeClinicianIds($data);

            $participant = $this->Participants->newEmptyEntity();

            $allowed = [
                'first_name','middle_name','last_name','preferred_name',
                'emergency_contact_name','emergency_contact_phone','emergency_contact_relation',
                'allergies','medicare_number',
                'email','phone',
                'login','participant_addresses','contacts',
            ];
            $filtered = array_intersect_key($data, array_flip($allowed));

            if (!empty($filtered['participant_addresses'])) {
                $filtered['participant_addresses'] = $this->normalizeAddresses($filtered['participant_addresses']);
            }

            $associated = ['Login','ParticipantAddresses'];
            if (!empty($filtered['contacts']) && is_array($filtered['contacts'])) {
                $filtered['contacts'] = $this->normalizeContacts($filtered['contacts']);
                $associated[] = 'Contacts';
                // derive fallback email/phone from primary contact
                $primaryEmail = $primaryPhone = null;
                $firstEmail   = $firstPhone   = null;
                foreach ($filtered['contacts'] as $c) {
                    $type  = strtolower($c['type'] ?? '');
                    $value = trim((string)($c['value'] ?? ''));
                    $isPri = !empty($c['is_primary']);
                    if ($type === 'email' && $value !== '') {
                        $firstEmail ??= $value;
                        if ($isPri) $primaryEmail = $value;
                    }
                    if ($type === 'phone' && $value !== '') {
                        $firstPhone ??= $value;
                        if ($isPri) $primaryPhone = $value;
                    }
                }
                $filtered['email'] ??= ($primaryEmail ?? $firstEmail);
                $filtered['phone'] ??= ($primaryPhone ?? $firstPhone);
            }

            $participant = $this->Participants->patchEntity($participant, $filtered, ['associated' => $associated]);

            if ($this->Participants->save($participant, ['associated' => $associated])) {
                $this->ensureParticipantLinkedToClinic((int)$participant->id, $clinicId);

                $name = trim((string)($participant->first_name ?? '') . ' ' . (string)($participant->last_name ?? ''));
                return $this->response->withType('json')
                    ->withStringBody(json_encode(['ok' => true, 'id' => (int)$participant->id, 'name' => $name]));
            }

            return $this->response->withStatus(422)->withType('json')
                ->withStringBody(json_encode([
                    'error'  => 'validation',
                    'errors' => $participant->getErrors(true),
                ]));
        }

        // HTML form create
        $clinicians       = $this->cliniciansForClinic($clinicId);
        $relationOptions  = [
            'parent'=>'Parent','spouse'=>'Spouse / Partner','sibling'=>'Sibling','child'=>'Child',
            'relative'=>'Relative','friend'=>'Friend','guardian'=>'Guardian','caregiver'=>'Caregiver','other'=>'Other (specify)',
        ];
        $participant = $this->Participants->newEmptyEntity();

        if ($this->request->is('post')) {
            $data = $this->request->getData();

            if (($data['emergency_contact_relation'] ?? '') === 'other') {
                $other = trim((string)($data['emergency_contact_relation_other'] ?? ''));
                if ($other !== '') $data['emergency_contact_relation'] = $other;
            }
            unset($data['emergency_contact_relation_other']);

            if (!empty($data['participant_addresses'])) {
                $data['participant_addresses'] = $this->normalizeAddresses($data['participant_addresses']);
            }

            $associated = ['Login','ParticipantAddresses'];
            if (isset($data['contacts']) && is_array($data['contacts'])) {
                $rows = $this->normalizeContacts($data['contacts']);
                $data['contacts'] = $rows;
                $associated[] = 'Contacts';

                foreach ($rows as $r) {
                    if (!empty($r['is_primary'])) {
                        if ($r['type'] === 'email' && empty($data['email'])) $data['email'] = $r['value'];
                        if ($r['type'] === 'phone' && empty($data['phone'])) $data['phone'] = $r['value'];
                        break;
                    }
                }
            }

            $participant = $this->Participants->patchEntity($participant, $data, ['associated' => $associated]);

            if ($this->Participants->save($participant, ['associated' => $associated])) {
                $this->ensureParticipantLinkedToClinic((int)$participant->id, $clinicId);
                $this->Flash->success('Participant has been created.');
                return $this->redirect(['action' => 'index']);
            }

            $errors   = $participant->getErrors(true);
            $htmlList = $this->formatErrorsAsList($errors);
            $this->Flash->error('Create failed. Please fix the following:' . $htmlList, ['escape' => false]);
        }

        $this->set(compact('participant', 'clinicians', 'relationOptions'));
    }

    public function edit(int $id)
    {
        $clinicId = (int)$this->currentClinicId();

        $participant = $this->scopeParticipantsToClinic(
            $this->Participants->find()->where(['Participants.id' => $id])->contain(['Login', 'Contacts', 'ParticipantAddresses']),
            $clinicId
        )->firstOrFail();

        $clinicians = $this->cliniciansForClinic($clinicId);
        $relationOptions = [
            'parent'=>'Parent','spouse'=>'Spouse / Partner','sibling'=>'Sibling','child'=>'Child',
            'relative'=>'Relative','friend'=>'Friend','guardian'=>'Guardian','caregiver'=>'Caregiver','other'=>'Other (specify)',
        ];

        if ($this->request->is(['patch','post','put'])) {
            $data    = $this->request->getData();
            $section = (string)($data['_section'] ?? '');
            unset($data['_section']);

            if (($data['emergency_contact_relation'] ?? '') === 'other') {
                $other = trim((string)($data['emergency_contact_relation_other'] ?? ''));
                if ($other !== '') $data['emergency_contact_relation'] = $other;
            }
            unset($data['emergency_contact_relation_other']);

            $data = $this->normalizeClinicianIds($data);

            if ($section === 'contact' && isset($data['contacts']) && is_array($data['contacts'])) {
                $data['contacts'] = $this->normalizeContacts($data['contacts']);
            }

            $patchOptions = [];
            $saveOptions  = [];

            if ($section === 'clinicians') {
                $patchOptions['associated'] = ['Login'];
                $saveOptions['associated']  = ['Login'];
                $data = ['login' => $data['login'] ?? ['_ids' => []]];
                unset($data['contacts'], $data['participant_addresses']);
            } elseif ($section === 'contact') {
                $patchOptions['associated'] = ['Contacts'];
                $saveOptions['associated']  = ['Contacts'];
                unset($data['login'], $data['participant_addresses']);
            } elseif ($section === 'addresses' || isset($data['participant_addresses'])) {
                $rows = $this->normalizeAddresses($data['participant_addresses'] ?? []);
                foreach ($rows as &$r) {
                    $r['participant_id'] = $id;
                }
                unset($r);

                $result = $this->saveAddresses($id, $rows);
                if ($result['ok']) {
                    $this->Flash->success('Addresses updated.');
                    return $this->redirect(['action' => 'edit', $id, '#' => 'card-addresses']);
                }
                $this->Flash->error($result['message'] ?? 'Update failed. Please check address section.');
                return $this->redirect(['action' => 'edit', $id, '#' => 'card-addresses']);
            } else {
                $whitelist = [
                    'personal'  => ['first_name','middle_name','last_name','preferred_name'],
                    'emergency' => ['emergency_contact_name','emergency_contact_phone','emergency_contact_relation'],
                    'misc'      => ['allergies','medicare_number'],
                ];
                if (isset($whitelist[$section])) {
                    $patchOptions['fields'] = $whitelist[$section];
                } else {
                    $patchOptions['associated'] = ['Login','Contacts'];
                    $saveOptions['associated']  = ['Login','Contacts'];
                }
                unset($data['participant_addresses']);
            }

            $participant = $this->Participants->patchEntity($participant, $data, $patchOptions);

            if ($participant->getErrors()) {
                $this->Flash->error('Please enter the correct mobile phone number or Email address!');
            } else {
                if (
                    $section === 'contact'
                    || (isset($patchOptions['associated']) && in_array('Contacts', (array)$patchOptions['associated'], true))
                ) {
                    $primary = null;
                    foreach ((array)($participant->contacts ?? []) as $c) {
                        if ((int)$c->is_primary === 1) { $primary = $c; break; }
                    }
                    if ($primary) {
                        if ($primary->type === 'email') $participant->email = $primary->value;
                        else $participant->phone = $primary->value;
                    }
                }

                if ($this->Participants->save($participant, $saveOptions)) {
                    $this->ensureParticipantLinkedToClinic((int)$participant->id, $clinicId);
                    $this->Flash->success('Participant has been updated.');
                    return $this->redirect(['action' => 'edit', $id, '#' => 'card-' . ($section ?: 'personal')]);
                }

                Log::error('[Participants.edit] Save failed: ' . json_encode($participant->getErrors(), JSON_UNESCAPED_UNICODE));
                $this->Flash->error('Update failed. Please check the form and try again.');
            }
        }

        $this->set(compact('participant', 'clinicians', 'relationOptions'));
    }

    public function delete(int $id): ?Response
    {
        $this->request->allowMethod(['post','delete']);
        $clinicId = (int)$this->currentClinicId();

        // Only delete if the participant belongs to the current clinic
        $entity = $this->scopeParticipantsToClinic(
            $this->Participants->find()->where(['Participants.id' => $id]),
            $clinicId
        )->firstOrFail();

        if ($this->Participants->delete($entity)) {
            $this->Flash->success('Participant has been deleted.');
        } else {
            $this->Flash->error('Delete failed.');
        }

        return $this->redirect(['action' => 'index']);
    }

    public function export(): Response
    {
        $this->request->allowMethod(['get']);
        $clinicId = (int)$this->currentClinicId();

        $q = trim((string)$this->request->getQuery('q', ''));

        $find = $this->scopeParticipantsToClinic(
            $this->Participants->find(),
            $clinicId
        );

        if ($q !== '') {
            $find->where([
                'OR' => [
                    'first_name LIKE'  => "%{$q}%",
                    'last_name LIKE'   => "%{$q}%",
                    'email LIKE'       => "%{$q}%",
                    'phone LIKE'       => "%{$q}%",
                    'emergency_contact_name LIKE'     => "%{$q}%",
                    'emergency_contact_phone LIKE'    => "%{$q}%",
                    'emergency_contact_relation LIKE' => "%{$q}%",
                ],
            ]);
        }

        $rows = $find->orderAsc('last_name')->orderAsc('first_name')->all();

        $fh = fopen('php://temp', 'r+');
        fputcsv($fh, [
            'ID','First Name','Middle Name','Last Name','Preferred Name','Email','Phone',
            'Emergency Name','Emergency Phone','Emergency Relation',
            'Created','Modified',
        ]);

        foreach ($rows as $r) {
            fputcsv($fh, [
                $r->id,
                (string)$r->first_name,
                (string)$r->middle_name,
                (string)$r->last_name,
                (string)$r->preferred_name,
                (string)$r->email,
                (string)$r->phone,
                (string)$r->emergency_contact_name,
                (string)$r->emergency_contact_phone,
                (string)$r->emergency_contact_relation,
                $r->created?->i18nFormat('yyyy-MM-dd HH:mm'),
                $r->modified?->i18nFormat('yyyy-MM-dd HH:mm'),
            ]);
        }
        rewind($fh);
        $csv = stream_get_contents($fh);
        fclose($fh);

        return $this->response
            ->withType('csv')
            ->withDownload('participants.csv')
            ->withStringBody($csv);
    }

    public function suggest(): Response
    {
        $this->request->allowMethod(['get']);
        $clinicId = (int)$this->currentClinicId();

        $q = trim((string)$this->request->getQuery('q', ''));
        $find = $this->scopeParticipantsToClinic(
            $this->Participants->find()
                ->select(['id','first_name','middle_name','last_name','preferred_name','email']),
            $clinicId
        );

        if ($q !== '') {
            $like = '%' . str_replace(['%','_'], ['\%','\_'], $q) . '%';
            $find->where([
                'OR' => [
                    'first_name LIKE'     => $like,
                    'middle_name LIKE'    => $like,
                    'last_name LIKE'      => $like,
                    'preferred_name LIKE' => $like,
                    'email LIKE'          => $like,
                ],
            ]);
        }

        $rows = $find->orderAsc('first_name')->limit(20)->all();

        $out = [];
        foreach ($rows as $r) {
            $parts = array_filter([
                (string)$r->first_name,
                (string)($r->middle_name ?? ''),
                (string)$r->last_name,
            ], fn($s) => trim($s) !== '');
            $full = trim(preg_replace('/\s+/', ' ', implode(' ', $parts)));

            $display = $full !== ''
                ? $full
                : (trim((string)($r->preferred_name ?? '')) !== '' ? (string)$r->preferred_name : (string)$r->email);

            $out[] = [
                'id'         => (int)$r->id,
                'name'       => $display,
                'email'      => (string)$r->email,
                'full_name'  => $full,
                'preferred'  => (string)($r->preferred_name ?? ''),
                'first_name' => (string)$r->first_name,
                'middle_name'=> (string)($r->middle_name ?? ''),
                'last_name'  => (string)$r->last_name,
            ];
        }

        return $this->response->withType('json')
            ->withStringBody(json_encode($out));
    }

    public function history(int $id)
    {
        $clinicId    = (int)$this->currentClinicId();
        $participant = $this->scopeParticipantsToClinic(
            $this->Participants->find()->where(['Participants.id' => $id])->contain(['Login']),
            $clinicId
        )->firstOrFail();

        $Events = $this->fetchTable('CalendarEvents');

        $q = $Events->find()
            ->select(['id','title','start','end','location','status','appointment_type','user_id','description'])
            ->orderDesc('start');

        // If your events are also clinic scoped, you could add a join to clinic here as well.
        if ($Events->hasField('participant_id')) {
            $q->where(['participant_id' => $id]);
        } else {
            $fullname = trim(($participant->first_name ?? '') . ' ' . ($participant->last_name ?? ''));
            $q->where(['title' => $fullname]);
        }

        $events = $q->all();

        $clinNames = [];
        try {
            $clinNames = $this->fetchTable('Login')
                ->find('list', keyField: 'id', valueField: 'name')
                ->toArray();
        } catch (Throwable $e) {}

        $this->set(compact('participant', 'events', 'clinNames'));
    }

    public function dashboard(int $id)
    {
        $clinicId = (int)$this->currentClinicId();

        $participant = $this->scopeParticipantsToClinic(
            $this->Participants->find()
                ->where(['Participants.id' => $id])
                ->contain(['Login', 'Contacts', 'ParticipantAddresses']),
            $clinicId
        )->firstOrFail();

        $Events = $this->fetchTable('CalendarEvents');
        $now    = new FrozenTime('now');

        $fullname = trim(($participant->first_name ?? '') . ' ' . ($participant->last_name ?? ''));
        $condBase = $Events->hasField('participant_id')
            ? ['participant_id' => $id]
            : ($fullname !== '' ? ['title' => $fullname] : ['id <' => 0]);

        $totalCount = (int)$Events->find()->where($condBase)->count();

        $lastEvt = $Events->find()
            ->select(['id','title','start','end','location','status'])
            ->where($condBase + ['start <=' => $now])
            ->orderDesc('start')->first();

        $nextEvt = $Events->find()
            ->select(['id','title','start','end','location','status'])
            ->where($condBase + ['start >=' => $now])
            ->orderAsc('start')->first();

        $stats = [
            'total'   => $totalCount,
            'last'    => $lastEvt,
            'next'    => $nextEvt,
            'balance' => null,
        ];

        $events = $Events->find()
            ->select(['id','title','start','end','location','status','appointment_type'])
            ->where($condBase + ['start >=' => $now])
            ->orderAsc('start')
            ->limit(5)
            ->all();

        $baseDir = WWW_ROOT . 'uploads' . DS . 'participants' . DS . $id . DS;
        if (!is_dir($baseDir)) {
            @mkdir($baseDir, 0775, true);
        }
        $files = [];
        if (is_dir($baseDir)) {
            foreach (scandir($baseDir) ?: [] as $fn) {
                if ($fn === '.' || $fn === '..') continue;
                $path = $baseDir . $fn;
                if (is_file($path)) {
                    $files[] = ['name' => $fn,'size' => filesize($path),'mtime' => filemtime($path)];
                }
            }
            usort($files, fn($a, $b) => $b['mtime'] <=> $a['mtime']);
        }

        $this->set(compact('participant', 'stats', 'events', 'files'));
        $this->viewBuilder()->setTemplate('dashboard');
    }

    public function uploadFile(int $id)
    {
        $clinicId = (int)$this->currentClinicId();
        // Ensure participant belongs to clinic
        $this->scopeParticipantsToClinic(
            $this->Participants->find()->where(['Participants.id' => $id]),
            $clinicId
        )->firstOrFail();

        $this->request->allowMethod(['post']);

        $baseDir = WWW_ROOT . 'uploads' . DS . 'participants' . DS . $id . DS;
        if (!is_dir($baseDir)) {
            @mkdir($baseDir, 0775, true);
        }

        $uploads = $this->request->getUploadedFiles();
        $files   = $uploads['files'] ?? null;

        $ok = 0; $fail = 0;
        $allowed = ['pdf','jpg','jpeg','png','heic','doc','docx','xls','xlsx','txt','rtf'];
        $maxSize = 20 * 1024 * 1024;

        $files = is_array($files) ? $files : ($files ? [$files] : []);
        foreach ($files as $f) {
            if (!$f || $f->getError() !== UPLOAD_ERR_OK) { $fail++; continue; }
            if ($f->getSize() > $maxSize) { $fail++; continue; }

            $orig = $f->getClientFilename() ?? 'file';
            $name = preg_replace('/[^\w.\- ]+/u', '_', $orig);
            $ext  = strtolower(pathinfo($name, PATHINFO_EXTENSION));
            if ($ext && !in_array($ext, $allowed, true)) { $fail++; continue; }

            $target = $baseDir . date('Ymd_His') . '_' . $name;
            try { $f->moveTo($target); $ok++; } catch (Throwable $e) { $fail++; }
        }

        if ($ok > 0)   $this->Flash->success("$ok file is uploaded.");
        if ($fail > 0) $this->Flash->error("$fail file is failed to upload.");

        return $this->redirect(['action' => 'dashboard',$id,'#' => 'files']);
    }

    public function downloadFile(int $id, string $name)
    {
        $clinicId = (int)$this->currentClinicId();
        $this->scopeParticipantsToClinic(
            $this->Participants->find()->where(['Participants.id' => $id]),
            $clinicId
        )->firstOrFail();

        $this->request->allowMethod(['get']);
        $safe = preg_replace('/[^\w.\- ]+/u', '_', $name);
        $path = WWW_ROOT . 'uploads' . DS . 'participants' . DS . $id . DS . $safe;
        if (!is_file($path)) {
            $this->Flash->error('File not found.');
            return $this->redirect(['action' => 'dashboard',$id,'#' => 'files']);
        }

        return $this->response->withFile($path, ['download' => true, 'name' => $safe]);
    }

    public function deleteFile(int $id)
    {
        $clinicId = (int)$this->currentClinicId();
        $this->scopeParticipantsToClinic(
            $this->Participants->find()->where(['Participants.id' => $id]),
            $clinicId
        )->firstOrFail();

        $this->request->allowMethod(['post','delete']);
        $name = (string)$this->request->getData('name');
        $safe = preg_replace('/[^\w.\- ]+/u', '_', $name);
        $path = WWW_ROOT . 'uploads' . DS . 'participants' . DS . $id . DS . $safe;

        if ($safe === '' || !is_file($path)) {
            $this->Flash->error('File not found.');
            return $this->redirect(['action' => 'dashboard',$id,'#' => 'files']);
        }
        if (@unlink($path)) $this->Flash->success('File deleted.');
        else               $this->Flash->error('Delete failed.');

        return $this->redirect(['action' => 'dashboard',$id,'#' => 'files']);
    }

    public function locations(): Response
    {
        $this->request->allowMethod(['get']);
        $clinicId = (int)$this->currentClinicId();
        $pid = (int)$this->request->getQuery('participant_id');
        if ($pid <= 0) {
            return $this->response->withStatus(400)->withType('json')
                ->withStringBody(json_encode(['error' => 'participant_id required']));
        }

        // Verify participant belongs to clinic
        $this->scopeParticipantsToClinic(
            $this->Participants->find()->where(['Participants.id' => $pid]),
            $clinicId
        )->firstOrFail();

        $rows = $this->ParticipantAddresses->find()
            ->where(['participant_id' => $pid])
            ->orderAsc('address_type')
            ->all();

        $fmt = function ($a) {
            $l1 = trim((string)($a->address_line1 ?? ''));
            $l2 = trim((string)($a->address_line2 ?? ''));
            $city = trim((string)($a->city ?? ''));
            $reg  = trim((string)($a->region ?? ''));
            $pc   = trim((string)($a->postcode ?? ''));
            $cty  = trim((string)($a->country ?? ''));
            $parts = [];
            if ($l1 !== '') $parts[] = $l1;
            if ($l2 !== '') $parts[] = $l2;
            $tail = trim($city . ' ' . $reg . ' ' . $pc);
            if ($tail !== '') $parts[] = $tail;
            if ($cty !== '')  $parts[] = $cty;
            return implode(', ', array_filter($parts));
        };

        $labelMap = ['home' => 'Home','work' => 'Work','other' => 'Other'];
        $out = [];
        foreach ($rows as $r) {
            $type  = strtolower((string)($r->address_type ?? 'other'));
            $label = $labelMap[$type] ?? ucfirst($type);
            $addr  = $fmt($r);
            if ($addr === '') continue;

            $out[] = [
                'id'    => (int)$r->id,
                'type'  => $type,
                'text'  => $label . ' — ' . $addr,
                'value' => $addr,
            ];
        }

        return $this->response->withType('json')->withStringBody(json_encode($out));
    }

    public function contacts(): Response
    {
        $this->request->allowMethod(['get']);
        $clinicId = (int)$this->currentClinicId();
        $pid = (int)$this->request->getQuery('participant_id');
        if ($pid <= 0) {
            return $this->response->withType('json')->withStringBody(json_encode([]));
        }

        $participant = $this->scopeParticipantsToClinic(
            $this->Participants->find()->where(['Participants.id' => $pid])->contain(['Contacts', 'Login']),
            $clinicId
        )->firstOrFail();

        $out = [];
        foreach ((array)($participant->contacts ?? []) as $c) {
            $type = strtolower((string)$c->type);
            if (!in_array($type, ['email','phone'], true)) continue;
            $val  = trim((string)$c->value);
            if ($val === '') continue;

            $out[] = [
                'type'          => $type,
                'value'         => $val,
                'label'         => (string)($c->label ?? ''),
                'is_primary'    => (int)($c->is_primary ? 1 : 0),
                'is_emergency'  => (int)($c->is_emergency ? 1 : 0),
            ];
        }

        $pEmail = trim((string)($participant->email ?? ''));
        if ($pEmail !== '') $out[] = ['type' => 'email','value' => $pEmail,'label' => 'profile','is_primary' => 0, 'is_emergency' => 0];
        $pPhone = trim((string)($participant->phone ?? ''));
        if ($pPhone !== '') $out[] = ['type' => 'phone','value' => $pPhone,'label' => 'profile','is_primary' => 0, 'is_emergency' => 0];

        foreach ((array)($participant->login ?? []) as $u) {
            $uEmail = trim((string)($u->email ?? ''));
            if ($uEmail !== '') $out[] = ['type' => 'email','value' => $uEmail,'label' => 'login','is_primary' => 0, 'is_emergency' => 0];
        }

        $seen = [];
        $dedup = [];
        foreach ($out as $row) {
            $k = $row['type'] . '|' . strtolower($row['value']);
            if (!isset($seen[$k]) || $row['is_primary'] === 1) {
                $seen[$k]  = true;
                $dedup[$k] = $row;
            }
        }
        $out = array_values($dedup);

        usort($out, function ($a, $b) {
            if ($a['is_primary']    !== $b['is_primary'])   return $a['is_primary']    ? -1 : 1;
            if ($a['is_emergency']  !== $b['is_emergency']) return $a['is_emergency']  ? -1 : 1;
            if ($a['type'] !== $b['type']) return $a['type'] === 'email' ? -1 : 1;
            return strcasecmp($a['value'], $b['value']);
        });

        return $this->response->withType('json')->withStringBody(json_encode($out));
    }

    private function normalizeContacts(array $rows): array
    {
        $primaryIdx = null;
        if (isset($rows['_primary_index'])) {
            $primaryIdx = (string)$rows['_primary_index'];
            unset($rows['_primary_index']);
        }

        $out = [];
        foreach ($rows as $idx => $r) {
            if (!is_array($r)) continue;

            if (!empty($r['_delete'])) {
                $r['_delete'] = 1;
                $out[] = $r;
                continue;
            }

            $type  = strtolower(trim((string)($r['type'] ?? '')));
            $value = trim((string)($r['value'] ?? ''));

            if ($type === '' || $value === '') continue;

            $r['type']       = in_array($type, ['phone','email'], true) ? $type : 'phone';
            $r['label']      = trim((string)($r['label'] ?? ''));
            $r['is_primary'] = (string)$idx === $primaryIdx ? 1 : 0;
            $r['is_emergency']  = !empty($r['is_emergency']) ? 1 : 0;
            $r['sort']       = isset($r['sort']) ? (int)$r['sort'] : $idx * 10;

            $out[] = $r;
        }

        if (!array_filter($out, fn($x) => !empty($x['is_primary']))) {
            if (isset($out[0])) $out[0]['is_primary'] = 1;
        }

        $seen = false;
        foreach ($out as &$row) {
            if (!$seen && !empty($row['is_primary'])) { $seen = true; $row['is_primary'] = 1; }
            else { $row['is_primary'] = 0; }
        }
        unset($row);

        return $out;
    }

    private function normalizeClinicianIds(array $data): array
    {
        if (isset($data['login']['_ids'])) return $data;

        if (isset($data['logins']['_ids']) && is_array($data['logins']['_ids'])) {
            $data['login']['_ids'] = array_values(
                array_unique(array_map('intval', (array)$data['logins']['_ids'])),
            );
            unset($data['logins']);
            return $data;
        }

        $ids = [];
        if (!empty($data['clinician_ids']) && is_array($data['clinician_ids'])) {
            $ids = $data['clinician_ids'];
            unset($data['clinician_ids']);
        } elseif (!empty($data['login_ids']) && is_array($data['login_ids'])) {
            $ids = $data['login_ids'];
            unset($data['login_ids']);
        }

        if ($ids) {
            $data['login']['_ids'] = array_values(
                array_unique(array_map('intval', (array)$ids)),
            );
        }

        return $data;
    }

    private function normalizeAddresses($rows): array
    {
        if (!is_array($rows)) return [];

        $out = [];
        foreach ($rows as $r) {
            if (!is_array($r)) continue;
            $r = array_map(fn($v) => is_string($v) ? trim($v) : $v, $r);

            $allEmpty = empty($r['address_line1']) && empty($r['address_line2'])
                && empty($r['city']) && empty($r['region'])
                && empty($r['postcode']) && empty($r['country']);

            if ($allEmpty && empty($r['id'])) continue;

            $r['address_type'] = in_array(($r['address_type'] ?? 'home'), ['home','work','other'], true)
                ? $r['address_type'] : 'home';

            $out[] = [
                'id'            => (isset($r['id']) && $r['id'] !== '' ? (int)$r['id'] : null),
                'address_type'  => $r['address_type']  ?? 'home',
                'address_line1' => $r['address_line1'] ?? null,
                'address_line2' => $r['address_line2'] ?? null,
                'city'          => $r['city']          ?? null,
                'region'        => $r['region']        ?? null,
                'postcode'      => $r['postcode']      ?? null,
                'country'       => $r['country']       ?? 'Australia',
                'time_zone'     => $r['time_zone']     ?? null,
                '_delete'       => !empty($r['_delete']) ? 1 : 0,
            ];
        }

        return $out;
    }

    private function saveAddresses(int $participantId, array $rows): array
    {
        $Addresses = $this->ParticipantAddresses;

        $types = [];
        foreach ($rows as $r) {
            if (!empty($r['_delete'])) continue;
            $t = strtolower(trim((string)($r['address_type'] ?? '')));
            if ($t === '') continue;
            if (isset($types[$t]) && (int)($r['id'] ?? 0) === 0) {
                return ['ok' => false, 'message' => 'Duplicate address type "' . $t . '".'];
            }
            $types[$t] = true;
        }

        $conn = $Addresses->getConnection();
        $conn->begin();

        $errors = [];
        try {
            $existing = $Addresses->find()
                ->where(['participant_id' => $participantId])
                ->all()
                ->indexBy('id')
                ->toArray();

            foreach ($rows as $r) {
                $id  = isset($r['id']) && $r['id'] !== '' ? (int)$r['id'] : null;
                $del = !empty($r['_delete']);
                unset($r['_delete']);

                $r['participant_id'] = $participantId;

                if ($id) {
                    $entity = $existing[$id] ?? null;
                    if (!$entity) continue;
                    if ($del) {
                        if (!$Addresses->delete($entity)) {
                            $errors[$id]['_delete'] = ['Delete failed'];
                        }
                        continue;
                    }
                    $patched = $Addresses->patchEntity($entity, $r);
                    if (!$Addresses->save($patched)) {
                        $errors[$id] = $patched->getErrors();
                    }
                } else {
                    if ($del) continue;
                    $new = $Addresses->newEntity($r);
                    if (!$Addresses->save($new)) {
                        $errors['new'][] = $new->getErrors();
                    }
                }
            }

            if ($errors) {
                $conn->rollback();
                return ['ok' => false, 'message' => 'Validation failed.', 'errors' => $errors];
            }

            $conn->commit();
            return ['ok' => true];
        } catch (Throwable $e) {
            $conn->rollback();
            return ['ok' => false, 'message' => $e->getMessage()];
        }
    }

    private function formatErrorsAsList(array $errors): string
    {
        $flat = [];
        $walk = function ($node, string $path = '') use (&$walk, &$flat) {
            foreach ($node as $key => $val) {
                $p = $path === ''
                    ? (is_int($key) ? '[' . $key . ']' : (string)$key)
                    : (is_int($key) ? $path . '[' . $key . ']' : $path . '.' . $key);

                if (is_array($val)) {
                    $hasString = false;
                    foreach ($val as $vv) {
                        if (is_string($vv)) { $hasString = true; break; }
                    }
                    if ($hasString) {
                        $flat[$p] = implode('； ', array_values($val));
                    } else {
                        $walk($val, $p);
                    }
                } else {
                    $flat[$p] = (string)$val;
                }
            }
        };
        $walk($errors, '');

        if (!$flat) return '';

        $sectionHint = function (string $path): string {
            if (strpos($path, 'contacts') === 0 || strpos($path, '[0].contacts') === 0) return '（Contact Detail）';
            if (strpos($path, 'participant_addresses') === 0) return '（Address）';
            return '';
        };

        $html = '<ul class="mb-0">';
        foreach ($flat as $k => $msg) {
            $html .= '<li><code>' . h($k) . '</code> ' . $sectionHint($k) . ' — ' . h($msg) . '</li>';
        }
        $html .= '</ul>';

        return $html;
    }

    /* Search helper kept for parity */
    public function search()
    {
        $this->request->allowMethod(['get']);
        $this->viewBuilder()->setClassName('Json');

        $query = $this->request->getQuery('q');
        $results = [];

        if (!empty($query)) {
            $clinicId = (int)$this->currentClinicId();

            $participants = $this->scopeParticipantsToClinic(
                $this->Participants->find()
                    ->select(['id', 'first_name', 'last_name', 'phone']),
                $clinicId
            )
                ->where([
                    'OR' => [
                        'first_name LIKE' => "%$query%",
                        'last_name LIKE'  => "%$query%",
                        'phone LIKE'      => "%$query%"
                    ]
                ])
                ->limit(10)
                ->toArray();

            foreach ($participants as $p) {
                $results[] = [
                    'id'    => $p->id,
                    'label' => trim(($p->first_name ?? '') . ' ' . ($p->last_name ?? '') . ' (' . ($p->phone ?? '') . ')')
                ];
            }
        }

        $this->set(compact('results'));
        $this->viewBuilder()->setOption('serialize', ['results']);
    }
}
