<?php

declare(strict_types=1);

final class SettingsController
{
    private const ROLES = PermissionService::ROLES;
    public static function general(): void
    {
        self::admin();
        render('settings_general.php', [
            'title' => t('settings.general_title'),
            'settings' => AppSettings::all(),
            'saved' => isset($_GET['saved']),
            'error' => (string) ($_GET['error'] ?? ''),
            'associationLogo' => AssociationBrand::path(),
        ]);
    }
    public static function saveGeneral(): void
    {
        self::admin();
        self::postOnly();
        $values = [];
        foreach (
            [
                'association_name',
                'association_address',
                'association_postal_code',
                'association_city',
                'association_phone',
                'association_email',
                'association_siret',
                'association_rna',
            ]
            as $key
        ) {
            $values[$key] = trim((string) ($_POST[$key] ?? ''));
        }
        if ($values['association_name'] === '') {
            http_response_code(400);
            echo h(t('settings.association_required'));
            return;
        }
        if ($values['association_email'] !== '' && !filter_var($values['association_email'], FILTER_VALIDATE_EMAIL)) {
            http_response_code(400);
            echo h(t('settings.invalid_email'));
            return;
        }
        $values['quarantine_days'] = (string) max(1, min(90, (int) ($_POST['quarantine_days'] ?? 15)));
        $values['backup_retention'] = (string) max(1, min(50, (int) ($_POST['backup_retention'] ?? 10)));
        $values['app_language'] = isset(I18n::LOCALES[(string) ($_POST['app_language'] ?? 'fr')])
            ? (string) $_POST['app_language']
            : 'fr';
        try {
            $fullAddress = implode(
                ', ',
                array_filter([
                    $values['association_address'],
                    trim($values['association_postal_code'] . ' ' . $values['association_city']),
                ]),
            );
            if ($fullAddress === '') {
                $values['association_latitude'] = '';
                $values['association_longitude'] = '';
            } elseif ($point = GeoService::geocode(DB::pdo(), $fullAddress)) {
                $values['association_latitude'] = (string) $point['lat'];
                $values['association_longitude'] = (string) $point['lng'];
            }
            if (isset($_POST['delete_association_logo'])) {
                AssociationBrand::delete();
            } elseif (isset($_FILES['association_logo'])) {
                AssociationBrand::store($_FILES['association_logo']);
            }
            AppSettings::save($values, Auth::id());
            AuditService::log(
                'settings_saved',
                '/settings/general/save',
                'Paramètres généraux et identité visuelle modifiés',
            );
            header('Location: /settings/general?saved=1');
        } catch (Throwable $e) {
            header('Location: /settings/general?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    public static function associationLogo(): void
    {
        self::admin();
        $path = AssociationBrand::path();
        if (!$path) {
            http_response_code(404);
            return;
        }
        $mime = (new finfo(FILEINFO_MIME_TYPE))->file($path) ?: 'application/octet-stream';
        header('Content-Type: ' . $mime);
        header('Content-Length: ' . filesize($path));
        header('Cache-Control: private, max-age=3600');
        header('X-Content-Type-Options: nosniff');
        readfile($path);
    }
    public static function users(): void
    {
        self::admin();
        $users = DB::pdo()
            ->query('SELECT * FROM users ORDER BY active DESC,display_name COLLATE NOCASE,username COLLATE NOCASE')
            ->fetchAll(PDO::FETCH_ASSOC);
        render('settings_users.php', ['title' => t('settings.users_title'), 'users' => $users, 'roles' => self::ROLES]);
    }
    public static function saveUser(): void
    {
        self::admin();
        if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
            http_response_code(405);
            return;
        }
        $db = DB::pdo();
        $id = (int) ($_POST['id'] ?? 0);
        $username = trim((string) ($_POST['username'] ?? ''));
        $display = trim((string) ($_POST['display_name'] ?? ''));
        $role = (string) ($_POST['role'] ?? 'lecture');
        $active = isset($_POST['active']) ? 1 : 0;
        $password = (string) ($_POST['password'] ?? '');
        if (!preg_match('/^[a-zA-Z0-9._-]{3,40}$/', $username) || $display === '' || !isset(self::ROLES[$role])) {
            http_response_code(400);
            echo h(t('settings.invalid_data'));
            return;
        }
        if ($id === Auth::id() && (!$active || $role !== 'admin')) {
            http_response_code(400);
            echo h(t('settings.keep_admin'));
            return;
        }
        if ($id) {
            $params = [':u' => $username, ':d' => $display, ':r' => $role, ':a' => $active, ':id' => $id];
            $sql = "UPDATE users SET username=:u,display_name=:d,role=:r,active=:a,updated_at=datetime('now')";
            if ($password !== '') {
                if (strlen($password) < 10) {
                    http_response_code(400);
                    echo h(t('settings.password_short'));
                    return;
                }
                $sql .= ',password_hash=:p';
                $params[':p'] = password_hash($password, PASSWORD_DEFAULT);
            }
            $sql .= ' WHERE id=:id';
            $db->prepare($sql)->execute($params);
            $summary = 'Utilisateur modifié : ' . $display;
        } else {
            if (strlen($password) < 10) {
                http_response_code(400);
                echo h(t('settings.password_required'));
                return;
            }
            $db->prepare(
                'INSERT INTO users(username,password_hash,display_name,role,active) VALUES(:u,:p,:d,:r,:a)',
            )->execute([
                ':u' => $username,
                ':p' => password_hash($password, PASSWORD_DEFAULT),
                ':d' => $display,
                ':r' => $role,
                ':a' => $active,
            ]);
            $id = (int) $db->lastInsertId();
            $summary = 'Utilisateur créé : ' . $display;
        }
        AuditService::log('user_saved', '/settings/users/save', $summary, 'user', $id, [
            'role' => $role,
            'active' => $active,
        ]);
        header('Location: /settings/users');
        exit();
    }
    public static function permissions(): void
    {
        self::admin();
        render('settings_permissions.php', [
            'title' => t('settings.permissions_title'),
            'roles' => PermissionService::ROLES,
            'modules' => PermissionService::MODULES,
            'matrix' => PermissionService::matrix(),
            'saved' => isset($_GET['saved']),
        ]);
    }
    public static function savePermissions(): void
    {
        self::admin();
        self::postOnly();
        PermissionService::save((array) ($_POST['permissions'] ?? []), Auth::id());
        AuditService::log('permissions_saved', '/settings/permissions/save', 'Permissions des rôles modifiées');
        header('Location: /settings/permissions?saved=1');
        exit();
    }
    public static function species(): void
    {
        self::admin();
        render('settings_species.php', [
            'title' => t('settings.species_title'),
            'species' => SpeciesService::all(),
            'categories' => SpeciesService::CATEGORIES,
            'saved' => isset($_GET['saved']),
            'error' => (string) ($_GET['error'] ?? ''),
        ]);
    }
    public static function rooms(): void
    {
        self::admin();
        render('settings_rooms.php', [
            'title' => t('rooms.title'),
            'rooms' => ShelterRoomService::all(),
            'saved' => isset($_GET['saved']),
            'error' => (string) ($_GET['error'] ?? ''),
        ]);
    }
    public static function saveRooms(): void
    {
        self::admin();
        self::postOnly();
        $db = DB::pdo();
        $submitted = (array) ($_POST['rooms'] ?? []);
        try {
            $db->beginTransaction();
            foreach ($submitted as $id => $row) {
                $id = (int) $id;
                if (!$id) {
                    continue;
                }
                $name = trim((string) ($row['name'] ?? ''));
                $type = (string) ($row['room_type'] ?? 'collective');
                $status = (string) ($row['status_code'] ?? 'refuge');
                $color = (string) ($row['color'] ?? '#6c757d');
                if (
                    $name === '' ||
                    !in_array($type, ['collective', 'boxes'], true) ||
                    !in_array($status, ['refuge', 'soin', 'quarantaine'], true) ||
                    !preg_match('/^#[0-9a-fA-F]{6}$/', $color)
                ) {
                    throw new RuntimeException(t('rooms.invalid'));
                }
                $db->prepare(
                    "UPDATE shelter_rooms SET name=?,room_type=?,status_code=?,color=?,bulk_validation=?,active=?,sort_order=?,updated_at=datetime('now') WHERE id=?",
                )->execute([
                    $name,
                    $type,
                    $status,
                    $color,
                    isset($row['bulk_validation']) ? 1 : 0,
                    isset($row['active']) ? 1 : 0,
                    max(0, (int) ($row['sort_order'] ?? 100)),
                    $id,
                ]);
                self::syncBoxes($db, $id, (string) ($row['boxes'] ?? ''));
            }
            $new = (array) ($_POST['new_room'] ?? []);
            $newName = trim((string) ($new['name'] ?? ''));
            if ($newName !== '') {
                $code = ShelterRoomService::normalizeCode((string) ($new['code'] ?? $newName));
                if (strlen($code) < 2) {
                    throw new RuntimeException(t('rooms.invalid'));
                }
                $type = in_array($new['room_type'] ?? '', ['collective', 'boxes'], true)
                    ? (string) $new['room_type']
                    : 'collective';
                $status = in_array($new['status_code'] ?? '', ['refuge', 'soin', 'quarantaine'], true)
                    ? (string) $new['status_code']
                    : 'refuge';
                $color = preg_match('/^#[0-9a-fA-F]{6}$/', (string) ($new['color'] ?? ''))
                    ? (string) $new['color']
                    : '#6c757d';
                $db->prepare(
                    'INSERT INTO shelter_rooms(code,name,room_type,status_code,color,bulk_validation,sort_order) VALUES(?,?,?,?,?,?,?)',
                )->execute([$code, $newName, $type, $status, $color, isset($new['bulk_validation']) ? 1 : 0, 100]);
                self::syncBoxes($db, (int) $db->lastInsertId(), (string) ($new['boxes'] ?? ''));
            }
            $db->commit();
            AuditService::log('rooms_saved', '/settings/rooms/save', 'Salles et box du refuge modifiés');
            header('Location: /settings/rooms?saved=1');
        } catch (Throwable $e) {
            if ($db->inTransaction()) {
                $db->rollBack();
            }
            header('Location: /settings/rooms?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    private static function syncBoxes(PDO $db, int $roomId, string $lines): void
    {
        $names = array_values(
            array_filter(array_map('trim', preg_split('/\R/u', $lines) ?: []), static fn($name) => $name !== ''),
        );
        $existing = $db->prepare('SELECT id,code FROM shelter_boxes WHERE room_id=? ORDER BY sort_order,id');
        $existing->execute([$roomId]);
        $known = $existing->fetchAll(PDO::FETCH_ASSOC);
        $kept = [];
        foreach ($names as $index => $name) {
            $position = ($index + 1) * 10;
            if (isset($known[$index])) {
                $id = (int) $known[$index]['id'];
                $kept[] = $id;
                $db->prepare(
                    "UPDATE shelter_boxes SET name=?,active=1,sort_order=?,updated_at=datetime('now') WHERE id=?",
                )->execute([$name, $position, $id]);
                continue;
            }
            $code = ShelterRoomService::normalizeCode($name) ?: 'box-' . ($index + 1);
            $base = $code;
            $suffix = 2;
            $exists = $db->prepare('SELECT 1 FROM shelter_boxes WHERE room_id=? AND code=?');
            while (true) {
                $exists->execute([$roomId, $code]);
                if (!$exists->fetchColumn()) {
                    break;
                }
                $code = $base . '-' . $suffix++;
            }
            $db->prepare('INSERT INTO shelter_boxes(room_id,code,name,sort_order) VALUES(?,?,?,?)')->execute([
                $roomId,
                $code,
                $name,
                $position,
            ]);
            $kept[] = (int) $db->lastInsertId();
        }
        foreach ($known as $box) {
            if (!in_array((int) $box['id'], $kept, true)) {
                $db->prepare("UPDATE shelter_boxes SET active=0,updated_at=datetime('now') WHERE id=?")->execute([
                    (int) $box['id'],
                ]);
            }
        }
    }
    public static function saveSpecies(): void
    {
        self::admin();
        self::postOnly();
        $db = DB::pdo();
        $id = (int) ($_POST['id'] ?? 0);
        $name = trim((string) ($_POST['name'] ?? ''));
        $code = SpeciesService::normalize((string) ($_POST['code'] ?? ''));
        $category = (string) ($_POST['category'] ?? 'autre');
        $icon = trim((string) ($_POST['icon'] ?? '')) ?: '🐾';
        $color = trim((string) ($_POST['color'] ?? '#6c757d'));
        $active = isset($_POST['active']) ? 1 : 0;
        $sort = max(0, min(999, (int) ($_POST['sort_order'] ?? 100)));
        if (
            $name === '' ||
            !preg_match('/^[a-z0-9][a-z0-9-]{1,49}$/', $code) ||
            !isset(SpeciesService::CATEGORIES[$category]) ||
            !preg_match('/^#[0-9a-fA-F]{6}$/', $color)
        ) {
            header('Location: /settings/species?error=' . rawurlencode(t('settings.species_invalid')));
            exit();
        }
        try {
            if ($id > 0) {
                $old = $db->prepare('SELECT code,is_system FROM ref_species WHERE id=:id');
                $old->execute([':id' => $id]);
                $oldRow = $old->fetch(PDO::FETCH_ASSOC);
                if (!$oldRow) {
                    throw new RuntimeException(t('settings.species_not_found'));
                }
                $oldCode = (string) $oldRow['code'];
                if ((int) $oldRow['is_system'] === 1) {
                    $code = $oldCode;
                }
                $db->beginTransaction();
                $db->prepare(
                    'UPDATE ref_species SET code=:code,name=:name,category=:category,icon=:icon,color=:color,active=:active,sort_order=:sort,updated_at=datetime(\'now\') WHERE id=:id',
                )->execute([
                    ':code' => $code,
                    ':name' => $name,
                    ':category' => $category,
                    ':icon' => $icon,
                    ':color' => $color,
                    ':active' => $active,
                    ':sort' => $sort,
                    ':id' => $id,
                ]);
                if ($oldCode !== $code) {
                    $db->prepare('UPDATE animals SET species=:new WHERE lower(trim(species))=lower(:old)')->execute([
                        ':new' => $code,
                        ':old' => $oldCode,
                    ]);
                }
                $db->commit();
            } else {
                $db->prepare(
                    'INSERT INTO ref_species(code,name,category,icon,color,active,sort_order) VALUES(:code,:name,:category,:icon,:color,:active,:sort)',
                )->execute([
                    ':code' => $code,
                    ':name' => $name,
                    ':category' => $category,
                    ':icon' => $icon,
                    ':color' => $color,
                    ':active' => $active,
                    ':sort' => $sort,
                ]);
            }
            AuditService::log(
                'species_saved',
                '/settings/species/save',
                'Espèce enregistrée : ' . $name,
                'species',
                $id ?: ((int) $db->lastInsertId()),
            );
            header('Location: /settings/species?saved=1');
        } catch (Throwable $e) {
            if ($db->inTransaction()) {
                $db->rollBack();
            }
            header('Location: /settings/species?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    public static function pricing(): void
    {
        self::admin();
        $db = DB::pdo();
        $clinics = $db
            ->query(
                "SELECT DISTINCT dc.id,dc.name FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id AND r.role IN ('cabinet','crematorium','fourriere') WHERE dc.kind='organization' AND dc.deleted_at IS NULL ORDER BY dc.name COLLATE NOCASE",
            )
            ->fetchAll(PDO::FETCH_ASSOC);
        $discounts = [];
        foreach ($db->query('SELECT * FROM clinic_discount_rules')->fetchAll(PDO::FETCH_ASSOC) as $r) {
            $discounts[(int) $r['clinic_contact_id']][(string) $r['discount_group']] = $r;
        }
        render('settings_pricing.php', [
            'title' => t('pricing.title'),
            'tariffs' => PricingService::tariffs(),
            'clinics' => $clinics,
            'discounts' => $discounts,
            'categories' => PricingService::CATEGORIES,
            'groups' => PricingService::GROUPS,
            'medications' => $db
                ->query('SELECT id,name FROM ref_medications ORDER BY name COLLATE NOCASE')
                ->fetchAll(PDO::FETCH_ASSOC),
            'vaccines' => $db
                ->query('SELECT id,name FROM ref_vaccines ORDER BY name COLLATE NOCASE')
                ->fetchAll(PDO::FETCH_ASSOC),
            'dewormers' => $db
                ->query('SELECT id,name FROM ref_dewormers ORDER BY name COLLATE NOCASE')
                ->fetchAll(PDO::FETCH_ASSOC),
            'settings' => AppSettings::all(),
            'saved' => isset($_GET['saved']),
            'error' => (string) ($_GET['error'] ?? ''),
        ]);
    }
    public static function saveTariff(): void
    {
        self::admin();
        self::postOnly();
        $db = DB::pdo();
        $id = (int) ($_POST['id'] ?? 0);
        $clinic = (int) ($_POST['clinic_contact_id'] ?? 0);
        $label = trim((string) ($_POST['label'] ?? ''));
        $category = (string) ($_POST['category'] ?? 'other');
        $group = (string) ($_POST['discount_group'] ?? 'act');
        $unit = trim((string) ($_POST['unit_label'] ?? 'acte')) ?: 'acte';
        $amount = PricingService::cents((string) ($_POST['amount'] ?? '0'));
        $active = isset($_POST['active']) ? 1 : 0;
        $eligible = $db->prepare(
            "SELECT 1 FROM directory_contacts dc WHERE dc.id=:id AND dc.kind='organization' AND dc.deleted_at IS NULL AND EXISTS(SELECT 1 FROM directory_contact_roles r WHERE r.contact_id=dc.id AND r.role IN ('cabinet','crematorium','fourriere'))",
        );
        $eligible->execute([':id' => $clinic]);
        if (
            !$clinic ||
            !$eligible->fetchColumn() ||
            $label === '' ||
            !isset(PricingService::CATEGORIES[$category]) ||
            !isset(PricingService::GROUPS[$group])
        ) {
            header('Location: /settings/pricing?error=' . rawurlencode(t('pricing.invalid_structure')));
            exit();
        }
        $params = [
            $clinic,
            mb_substr($label, 0, 180),
            $category,
            $group,
            mb_substr($unit, 0, 50),
            (int) ($_POST['medication_id'] ?? 0) ?: null,
            (int) ($_POST['vaccine_id'] ?? 0) ?: null,
            (int) ($_POST['dewormer_id'] ?? 0) ?: null,
            $amount,
            $active,
            trim((string) ($_POST['notes'] ?? '')) ?: null,
        ];
        try {
            if ($id) {
                $params[] = $id;
                $db->prepare(
                    "UPDATE clinic_tariffs SET clinic_contact_id=?,label=?,category=?,discount_group=?,unit_label=?,medication_id=?,vaccine_id=?,dewormer_id=?,amount_cents=?,active=?,notes=?,updated_at=datetime('now') WHERE id=?",
                )->execute($params);
            } else {
                $db->prepare(
                    'INSERT INTO clinic_tariffs(clinic_contact_id,label,category,discount_group,unit_label,medication_id,vaccine_id,dewormer_id,amount_cents,active,notes) VALUES(?,?,?,?,?,?,?,?,?,?,?)',
                )->execute($params);
                $id = (int) $db->lastInsertId();
            }
            AuditService::log(
                'tariff_saved',
                '/settings/pricing/tariff',
                'Tarif enregistré : ' . $label,
                'clinic_tariff',
                $id,
            );
            header('Location: /settings/pricing?saved=1');
        } catch (Throwable $e) {
            header('Location: /settings/pricing?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    public static function saveDiscount(): void
    {
        self::admin();
        self::postOnly();
        $clinic = (int) ($_POST['clinic_contact_id'] ?? 0);
        $submitted = (array) ($_POST['discounts'] ?? []);
        $db = DB::pdo();
        $eligible = $db->prepare(
            "SELECT 1 FROM directory_contacts dc WHERE dc.id=:id AND dc.kind='organization' AND dc.deleted_at IS NULL AND EXISTS(SELECT 1 FROM directory_contact_roles r WHERE r.contact_id=dc.id AND r.role IN ('cabinet','crematorium','fourriere'))",
        );
        $eligible->execute([':id' => $clinic]);
        if (!$clinic || !$eligible->fetchColumn()) {
            http_response_code(400);
            echo h(t('pricing.invalid_discount_structure'));
            return;
        }
        $stmt = $db->prepare(
            'INSERT INTO clinic_discount_rules(clinic_contact_id,discount_group,label,percent) VALUES(?,?,?,?) ON CONFLICT(clinic_contact_id,discount_group) DO UPDATE SET label=excluded.label,percent=excluded.percent',
        );
        $saved = [];
        try {
            $db->beginTransaction();
            foreach (PricingService::GROUPS as $group => $label) {
                if ($group === 'none' || !array_key_exists($group, $submitted)) {
                    continue;
                }
                $percent = max(0, min(100, (float) str_replace(',', '.', (string) $submitted[$group])));
                $stmt->execute([$clinic, $group, $label, $percent]);
                $saved[$group] = $percent;
            }
            $db->commit();
            AuditService::log(
                'discount_saved',
                '/settings/pricing/discount',
                'Remises tarifaires modifiées',
                'clinic',
                $clinic,
                $saved,
            );
            header('Location: /settings/pricing?saved=1');
        } catch (Throwable $e) {
            if ($db->inTransaction()) {
                $db->rollBack();
            }
            header('Location: /settings/pricing?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    public static function saveAdoptionFees(): void
    {
        self::admin();
        self::postOnly();
        AppSettings::save(
            [
                'adoption_fee_sterilized' => (string) max(
                    0,
                    min(2000, (int) ($_POST['adoption_fee_sterilized'] ?? 220)),
                ),
                'adoption_fee_unsterilized' => (string) max(
                    0,
                    min(2000, (int) ($_POST['adoption_fee_unsterilized'] ?? 200)),
                ),
            ],
            Auth::id(),
        );
        AuditService::log('adoption_fees_saved', '/settings/pricing/adoption', 'Tarifs d’adoption modifiés');
        header('Location: /settings/pricing?saved=1');
        exit();
    }
    public static function labReferences(): void
    {
        self::admin();
        $rows = LabReferenceService::all();
        $groups = [];
        foreach ($rows as $row) {
            $groups[$row['analyzer_name']][$row['species_code']][] = $row;
        }
        render('settings_lab_references.php', [
            'title' => t('lab_reference.title'),
            'groups' => $groups,
            'saved' => isset($_GET['saved']),
            'error' => (string) ($_GET['error'] ?? ''),
        ]);
    }
    public static function saveLabReferences(): void
    {
        self::admin();
        self::postOnly();
        $db = DB::pdo();
        $input = (array) ($_POST['ranges'] ?? []);
        $stmt = $db->prepare(
            "UPDATE lab_reference_ranges SET parameter_name=?,unit=?,reference_min=?,reference_max=?,source_note=?,active=?,updated_by=?,updated_at=datetime('now') WHERE id=?",
        );
        try {
            $db->beginTransaction();
            foreach ($input as $id => $row) {
                $id = (int) $id;
                $name = trim((string) ($row['name'] ?? ''));
                $unit = trim((string) ($row['unit'] ?? ''));
                $min = self::nullableNumber($row['min'] ?? '');
                $max = self::nullableNumber($row['max'] ?? '');
                if (!$id || $name === '' || $unit === '' || ($min !== null && $max !== null && $min > $max)) {
                    throw new RuntimeException(t('lab_reference.invalid'));
                }
                $stmt->execute([
                    mb_substr($name, 0, 120),
                    mb_substr($unit, 0, 30),
                    $min,
                    $max,
                    mb_substr(trim((string) ($row['source'] ?? '')), 0, 255) ?: null,
                    isset($row['active']) ? 1 : 0,
                    Auth::id(),
                    $id,
                ]);
            }
            $db->commit();
            AuditService::log(
                'lab_references_saved',
                '/settings/lab-references/save',
                'Intervalles de référence biologiques modifiés',
            );
            header('Location: /settings/lab-references?saved=1');
        } catch (RuntimeException $e) {
            if ($db->inTransaction()) {
                $db->rollBack();
            }
            header('Location: /settings/lab-references?error=' . rawurlencode($e->getMessage()));
        } catch (Throwable $e) {
            if ($db->inTransaction()) {
                $db->rollBack();
            }
            header('Location: /settings/lab-references?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    public static function publicSite(): void
    {
        self::admin();
        render('settings_public_site.php', [
            'title' => t('public_site.settings_title'),
            'settings' => AppSettings::all(),
            'saved' => isset($_GET['saved']),
            'error' => (string) ($_GET['error'] ?? ''),
        ]);
    }
    public static function health(): void
    {
        self::admin();
        render('settings_health.php', [
            'title' => t('health.page_title'),
            'report' => SystemHealthService::report(),
            'installed' => isset($_GET['installed']),
        ]);
    }
    public static function notifications(): void
    {
        self::admin();
        render('settings_notifications.php', [
            'title' => t('notification.page_title'),
            'settings' => AppSettings::all(),
            'saved' => isset($_GET['saved']),
            'sent' => (string) ($_GET['sent'] ?? ''),
        ]);
    }
    public static function saveNotifications(): void
    {
        self::admin();
        self::postOnly();
        $email = trim((string) ($_POST['notification_email_recipient'] ?? ''));
        if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            header('Location: /settings/notifications?sent=invalid');
            exit();
        }
        $frequency = in_array((string) ($_POST['notification_email_frequency'] ?? ''), ['daily', 'weekly'], true)
            ? (string) $_POST['notification_email_frequency']
            : 'weekly';
        AppSettings::save(
            [
                'notification_email_enabled' => isset($_POST['notification_email_enabled']) ? '1' : '0',
                'notification_email_frequency' => $frequency,
                'notification_email_recipient' => $email,
            ],
            Auth::id(),
        );
        AuditService::log('notifications_saved', '/settings/notifications', 'Configuration des notifications modifiée');
        header('Location: /settings/notifications?saved=1');
        exit();
    }
    public static function sendNotifications(): void
    {
        self::admin();
        self::postOnly();
        $result = NotificationService::send(true);
        AuditService::log(
            'notification_digest',
            '/settings/notifications/send',
            'Envoi manuel du résumé : ' . $result['reason'],
        );
        header('Location: /settings/notifications?sent=' . rawurlencode((string) $result['reason']));
        exit();
    }
    public static function savePublicSite(): void
    {
        self::admin();
        self::postOnly();
        $theme = (string) ($_POST['public_site_theme'] ?? 'warm');
        if (!in_array($theme, ['warm', 'nature', 'minimal'], true)) {
            $theme = 'warm';
        }
        $routing = (string) ($_POST['public_site_routing'] ?? 'integrated');
        if (!in_array($routing, ['integrated', 'root'], true)) {
            $routing = 'integrated';
        }
        $values = [
            'public_site_enabled' => isset($_POST['public_site_enabled']) ? '1' : '0',
            'public_site_theme' => $theme,
            'public_site_routing' => $routing,
            'public_site_about_enabled' => isset($_POST['public_site_about_enabled']) ? '1' : '0',
        ];
        foreach (
            [
                'public_site_home_title' => 150,
                'public_site_home_text' => 5000,
                'public_site_about_title' => 150,
                'public_site_about_text' => 10000,
                'public_site_donation_title' => 150,
                'public_site_donation_text' => 10000,
                'public_site_contact_text' => 5000,
                'public_site_legal_text' => 15000,
                'public_site_privacy_text' => 15000,
            ]
            as $key => $max
        ) {
            $values[$key] = mb_substr(trim((string) ($_POST[$key] ?? '')), 0, $max);
        }
        foreach (['public_site_donation_url', 'public_site_facebook_url', 'public_site_instagram_url'] as $key) {
            $url = trim((string) ($_POST[$key] ?? ''));
            $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
            if ($url !== '' && (!filter_var($url, FILTER_VALIDATE_URL) || $scheme !== 'https')) {
                header('Location: /settings/public-site?error=' . rawurlencode(t('public_site.invalid_url')));
                exit();
            }
            $values[$key] = $url;
        }
        AppSettings::save($values, Auth::id());
        AuditService::log('public_site_saved', '/settings/public-site/save', 'Configuration du site public modifiée');
        header('Location: /settings/public-site?saved=1');
        exit();
    }
    public static function audit(): void
    {
        self::admin();
        $rows = DB::pdo()
            ->query(
                'SELECT al.*,COALESCE(u.display_name,u.username) actor FROM audit_log al LEFT JOIN users u ON u.id=al.user_id ORDER BY al.id DESC LIMIT 500',
            )
            ->fetchAll(PDO::FETCH_ASSOC);
        render('settings_audit.php', ['title' => t('audit.title'), 'rows' => $rows]);
    }
    public static function dataTools(): void
    {
        self::admin();
        $db = DB::pdo();
        render('settings_data.php', [
            'title' => t('data.title'),
            'counts' => [
                'animals' => (int) $db->query('SELECT COUNT(*) FROM animals')->fetchColumn(),
                'contacts' => (int) $db->query('SELECT COUNT(*) FROM directory_contacts')->fetchColumn(),
            ],
            'status' => (string) ($_GET['status'] ?? ''),
            'backup' => (string) ($_GET['backup'] ?? ''),
            'error' => (string) ($_GET['error'] ?? ''),
        ]);
    }
    public static function resetData(): void
    {
        self::dataOperation(false);
    }
    public static function loadDemoData(): void
    {
        self::dataOperation(true);
    }
    private static function dataOperation(bool $demo): void
    {
        self::admin();
        self::postOnly();
        $expected = t($demo ? 'data.confirm_demo_phrase' : 'data.confirm_reset_phrase');
        if (trim((string) ($_POST['confirmation'] ?? '')) !== $expected) {
            header(
                'Location: /settings/data?error=' . rawurlencode(t('data.confirm_required', ['phrase' => $expected])),
            );
            exit();
        }
        $db = DB::pdo();
        try {
            $backup = BackupService::create($demo ? 'pre-demo-data' : 'pre-data-reset');
            $db->beginTransaction();
            $removed = DemoDataService::reset($db);
            $demo ? DemoDataService::seed($db, Auth::id()) : null;
            $db->commit();
            DemoDataService::clearFiles();
            if ($demo) {
                DemoDataService::installFiles($db);
            }
            AuditService::log(
                $demo ? 'demo_data_loaded' : 'business_data_reset',
                '/settings/data',
                $demo ? 'Base fictive de démonstration chargée' : 'Données métier remises à zéro',
                null,
                null,
                [
                    'removed_animals' => $removed['animals'],
                    'removed_contacts' => $removed['contacts'],
                    'backup' => $backup['name'],
                ],
            );
            header(
                'Location: /settings/data?status=' .
                    ($demo ? 'demo' : 'reset') .
                    '&backup=' .
                    rawurlencode($backup['name']),
            );
        } catch (Throwable $e) {
            if ($db->inTransaction()) {
                $db->rollBack();
            }
            header('Location: /settings/data?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    public static function backups(): void
    {
        self::admin();
        render('settings_backups.php', [
            'title' => t('backups.title'),
            'backups' => BackupService::list(),
            'status' => (string) ($_GET['status'] ?? ''),
            'error' => (string) ($_GET['error'] ?? ''),
        ]);
    }
    public static function createBackup(): void
    {
        self::admin();
        self::postOnly();
        try {
            $backup = BackupService::create();
            AuditService::log('backup_created', '/settings/backups/create', 'Sauvegarde créée : ' . $backup['name']);
            header('Location: /settings/backups?status=created');
        } catch (Throwable $e) {
            header('Location: /settings/backups?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    public static function saveBackupSchedule(): void
    {
        self::admin();
        self::postOnly();
        $frequency = (string) ($_POST['backup_schedule_frequency'] ?? 'daily');
        if (!in_array($frequency, ['daily', 'weekly'], true)) {
            $frequency = 'daily';
        }
        AppSettings::save(
            [
                'backup_schedule_enabled' => isset($_POST['backup_schedule_enabled']) ? '1' : '0',
                'backup_schedule_frequency' => $frequency,
            ],
            Auth::id(),
        );
        AuditService::log(
            'backup_schedule_saved',
            '/settings/backups/schedule',
            'Planification des sauvegardes modifiée',
        );
        header('Location: /settings/backups?status=schedule');
        exit();
    }
    public static function verifyBackup(): void
    {
        self::admin();
        self::postOnly();
        try {
            $result = BackupService::verify((string) ($_POST['file'] ?? ''));
            AppSettings::save(['backup_last_verified_at' => date('Y-m-d H:i:s')], Auth::id());
            AuditService::log(
                'backup_verified',
                '/settings/backups/verify',
                'Sauvegarde vérifiée : ' . $result['name'],
            );
            header('Location: /settings/backups?status=verified');
        } catch (RuntimeException $e) {
            header('Location: /settings/backups?error=' . rawurlencode($e->getMessage()));
        } catch (Throwable $e) {
            header('Location: /settings/backups?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    public static function downloadBackup(): void
    {
        self::admin();
        $path = BackupService::path((string) ($_GET['file'] ?? ''));
        if (!$path) {
            http_response_code(404);
            echo h(t('backups.not_found'));
            return;
        }
        PrivacyService::logAccess('backup', null, null, basename($path));
        AuditService::log('private_file_viewed', '/settings/backups/download', 'Sauvegarde privée téléchargée');
        header('Content-Type: application/zip');
        header('Content-Disposition: attachment; filename="' . basename($path) . '"');
        header('Content-Length: ' . filesize($path));
        header('Cache-Control: private,no-store');
        header('X-Content-Type-Options: nosniff');
        readfile($path);
    }
    public static function deleteBackup(): void
    {
        self::admin();
        self::postOnly();
        $name = (string) ($_POST['file'] ?? '');
        try {
            BackupService::delete($name);
            AuditService::log('backup_deleted', '/settings/backups/delete', 'Sauvegarde supprimée : ' . $name);
            header('Location: /settings/backups?status=deleted');
        } catch (RuntimeException $e) {
            header('Location: /settings/backups?error=' . rawurlencode($e->getMessage()));
        } catch (Throwable $e) {
            header('Location: /settings/backups?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    public static function restoreBackup(): void
    {
        self::admin();
        self::postOnly();
        $upload = $_FILES['backup'] ?? null;
        if (
            !is_array($upload) ||
            ($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK ||
            !is_uploaded_file((string) $upload['tmp_name'])
        ) {
            header('Location: /settings/backups?error=' . rawurlencode(t('backups.invalid_file')));
            exit();
        }
        if ((int) ($upload['size'] ?? 0) > 1_000_000_000) {
            header('Location: /settings/backups?error=' . rawurlencode(t('backups.too_large')));
            exit();
        }
        try {
            BackupService::restore((string) $upload['tmp_name']);
            Auth::logout();
            header('Location: /login?restored=1');
        } catch (RuntimeException $e) {
            header('Location: /settings/backups?error=' . rawurlencode($e->getMessage()));
        } catch (Throwable $e) {
            header('Location: /settings/backups?error=' . rawurlencode(SecurityService::publicError($e)));
        }
        exit();
    }
    private static function postOnly(): void
    {
        if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
            http_response_code(405);
            header('Allow: POST');
            exit();
        }
    }
    private static function nullableNumber(mixed $value): ?float
    {
        $value = str_replace(',', '.', trim((string) $value));
        if ($value === '') {
            return null;
        }
        if (!is_numeric($value)) {
            throw new RuntimeException(t('lab_reference.invalid'));
        }
        return (float) $value;
    }
    private static function admin(): void
    {
        if (!Auth::is('admin')) {
            http_response_code(403);
            echo h(t('settings.admin_only'));
            exit();
        }
    }
}
