<?php
class Reservation {
    // Type d'identifiant hw-address dans le schéma Kea de cette instance.
    // Vérifier avec : SELECT * FROM host_identifier_type;
    public const HW_ADDR_TYPE = 0; // hw-address selon host_identifier_type Kea

    // Options DHCP par hôte (dhcp4_options, scope_id = 4)
    private const HOST_SCOPE  = 4;
    private const OPT_ROUTERS = 3;   // gateway spécifique
    private const OPT_NTP     = 42;  // ntp-servers
    private const OPT_ROUTES  = 121; // classless-static-routes

    // Valeur sentinelle pour "ne pas envoyer de gateway" : écrase la gateway subnet sans en fournir une vraie
    private const NO_GATEWAY_VALUE = '0.0.0.0';

    /**
     * Liste toutes les réservations d'un subnet.
     * Retourne un tableau avec MAC formaté, IP lisible, description.
     */
    public static function getBySubnet(int $keaSubnetId): array {
        $stmt = Database::get()->prepare(
            'SELECT h.host_id,
                    HEX(h.dhcp_identifier) AS mac_hex,
                    h.ipv4_address,
                    h.hostname,
                    m.description,
                    m.short_lease,
                    m.created_at,
                    m.updated_at,
                    u1.username AS created_by,
                    u2.username AS updated_by,
                    EXISTS (
                        SELECT 1 FROM dhcp4_options o
                        WHERE o.host_id = h.host_id AND o.scope_id = ' . self::HOST_SCOPE . '
                          AND o.formatted_value IS NOT NULL AND o.formatted_value != \'\'
                    ) AS has_options,
                    EXISTS (
                        SELECT 1 FROM dhcp4_options o
                        WHERE o.host_id = h.host_id AND o.scope_id = ' . self::HOST_SCOPE . '
                          AND o.code = ' . self::OPT_NTP . '
                    ) AS has_ntp
             FROM hosts h
             LEFT JOIN app_reservation_meta m ON m.host_id = h.host_id
             LEFT JOIN app_users u1 ON u1.id = m.created_by_user_id
             LEFT JOIN app_users u2 ON u2.id = m.updated_by_user_id
             WHERE h.dhcp4_subnet_id = ?
               AND h.dhcp_identifier_type = ' . self::HW_ADDR_TYPE . '
             ORDER BY h.ipv4_address'
        );
        $stmt->execute([$keaSubnetId]);
        $rows = $stmt->fetchAll();

        foreach ($rows as &$row) {
            $row['mac']        = formatMacHex($row['mac_hex']);
            $row['ip_address'] = ((int)$row['ipv4_address'] === 0) ? '' : intToIp((int)$row['ipv4_address']);
        }
        return $rows;
    }

    public static function getById(int $hostId): ?array {
        $stmt = Database::get()->prepare(
            'SELECT h.host_id,
                    HEX(h.dhcp_identifier) AS mac_hex,
                    h.ipv4_address,
                    h.dhcp4_subnet_id,
                    h.hostname,
                    m.description,
                    m.short_lease
             FROM hosts h
             LEFT JOIN app_reservation_meta m ON m.host_id = h.host_id
             WHERE h.host_id = ? AND h.dhcp_identifier_type = ' . self::HW_ADDR_TYPE
        );
        $stmt->execute([$hostId]);
        $row = $stmt->fetch();
        if (!$row) return null;
        $row['mac']         = formatMacHex($row['mac_hex']);
        $row['ip_address']  = ((int)$row['ipv4_address'] === 0) ? '' : intToIp((int)$row['ipv4_address']);
        $row['short_lease'] = (int)($row['short_lease'] ?? 0);

        // Charger les options DHCP spécifiques à cet hôte
        $opts               = self::loadHostOptions($hostId);
        $row['no_gateway']  = isset($opts['_no_gateway']) ? 1 : 0;
        $row['gateway']     = $row['no_gateway'] ? '' : ($opts[self::OPT_ROUTERS] ?? '');
        $row['routes']      = isset($opts[self::OPT_ROUTES])
            ? self::parseRoutes($opts[self::OPT_ROUTES])
            : [];
        $row['ntp_enabled'] = isset($opts[self::OPT_NTP]) ? 1 : 0;
        return $row;
    }

    /**
     * Ajoute une réservation.
     * $data : ['mac', 'ip', 'hostname', 'description', 'gateway', 'routes']
     * Si $data['ip'] est vide (short-lease sans IP fixe), ipv4_address = 0.
     * Retourne le host_id créé.
     */
    public static function add(int $keaSubnetId, array $data): int {
        $db         = Database::get();
        $mac        = macToBinary($data['mac']);
        $ip         = !empty(trim($data['ip'] ?? '')) ? ipToInt($data['ip']) : 0;
        $shortLease = !empty($data['short_lease']) ? 1 : 0;

        $db->prepare(
            'INSERT INTO hosts
             (dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_address, hostname, dhcp4_client_classes)
             VALUES (?, ' . self::HW_ADDR_TYPE . ', ?, ?, ?, ?)'
        )->execute([
            $mac,
            $keaSubnetId,
            $ip,
            trim($data['hostname']),
            $shortLease ? 'short-lease' : null,
        ]);
        $hostId = (int)$db->lastInsertId();

        $db->prepare(
            'INSERT INTO app_reservation_meta
             (host_id, description, short_lease, created_by_user_id, created_at)
             VALUES (?, ?, ?, ?, NOW())'
        )->execute([$hostId, trim($data['description'] ?? ''), $shortLease, Auth::currentUserId()]);

        self::saveHostOptions($hostId, $data['gateway'] ?? '', $data['routes'] ?? [], $data['ntp_value'] ?? '', !empty($data['no_gateway']));

        return $hostId;
    }

    /**
     * Met à jour une réservation existante.
     */
    public static function update(int $hostId, array $data): void {
        $db         = Database::get();
        $mac        = macToBinary($data['mac']);
        $ip         = !empty(trim($data['ip'] ?? '')) ? ipToInt($data['ip']) : 0;
        $shortLease = !empty($data['short_lease']) ? 1 : 0;

        $db->prepare(
            'UPDATE hosts
             SET dhcp_identifier = ?, ipv4_address = ?, hostname = ?, dhcp4_client_classes = ?
             WHERE host_id = ?'
        )->execute([
            $mac,
            $ip,
            trim($data['hostname']),
            $shortLease ? 'short-lease' : null,
            $hostId,
        ]);

        // Upsert meta
        $db->prepare(
            'INSERT INTO app_reservation_meta
             (host_id, description, short_lease, created_by_user_id, created_at, updated_by_user_id, updated_at)
             VALUES (?, ?, ?, ?, NOW(), ?, NOW())
             ON DUPLICATE KEY UPDATE
               description        = VALUES(description),
               short_lease        = VALUES(short_lease),
               updated_by_user_id = VALUES(updated_by_user_id),
               updated_at         = NOW()'
        )->execute([
            $hostId,
            trim($data['description'] ?? ''),
            $shortLease,
            Auth::currentUserId(),
            Auth::currentUserId(),
        ]);

        self::saveHostOptions($hostId, $data['gateway'] ?? '', $data['routes'] ?? [], $data['ntp_value'] ?? '', !empty($data['no_gateway']));
    }

    public static function delete(int $hostId): void {
        $db = Database::get();
        $db->prepare('DELETE FROM dhcp4_options WHERE host_id = ? AND scope_id = ?')
           ->execute([$hostId, self::HOST_SCOPE]);
        $db->prepare('DELETE FROM app_reservation_meta WHERE host_id = ?')->execute([$hostId]);
        $db->prepare('DELETE FROM hosts WHERE host_id = ?')->execute([$hostId]);
    }

    /** Supprime toutes les réservations d'un subnet Kea (hosts + meta + options hôte). */
    public static function deleteBySubnet(int $keaSubnetId): int {
        $db   = Database::get();
        $ids  = $db->prepare(
            'SELECT host_id FROM hosts WHERE dhcp4_subnet_id = ? AND dhcp_identifier_type = ' . self::HW_ADDR_TYPE
        );
        $ids->execute([$keaSubnetId]);
        $hostIds = $ids->fetchAll(PDO::FETCH_COLUMN);
        if (!$hostIds) return 0;
        $in = implode(',', array_map('intval', $hostIds));
        $db->exec("DELETE FROM dhcp4_options WHERE host_id IN ($in) AND scope_id = " . self::HOST_SCOPE);
        $db->exec("DELETE FROM app_reservation_meta WHERE host_id IN ($in)");
        $db->exec("DELETE FROM hosts WHERE host_id IN ($in)");
        return count($hostIds);
    }

    /**
     * Recherche multi-subnet sur hostname, MAC, description.
     * $term : chaîne brute (MAC avec : ou - acceptés).
     * Retourne les résultats enrichis avec subnet_name, subnet_app_id.
     */
    public static function search(string $term): array {
        $term = trim($term);
        if ($term === '') return [];

        $db = Database::get();

        // Normaliser un éventuel MAC (séparateurs : ou -)
        $hexOnly = preg_replace('/[^0-9a-fA-F]/', '', $term);
        $isMac   = strlen($hexOnly) === 12;

        if ($isMac) {
            // Recherche exacte sur le binaire
            $binary = hex2bin($hexOnly);
            $stmt   = $db->prepare(
                'SELECT h.host_id,
                        HEX(h.dhcp_identifier) AS mac_hex,
                        h.ipv4_address,
                        h.dhcp4_subnet_id,
                        h.hostname,
                        m.description,
                        m.short_lease,
                        EXISTS (
                            SELECT 1 FROM dhcp4_options o
                            WHERE o.host_id = h.host_id AND o.scope_id = ' . self::HOST_SCOPE . '
                              AND o.formatted_value IS NOT NULL AND o.formatted_value != \'\'
                        ) AS has_options,
                        EXISTS (
                            SELECT 1 FROM dhcp4_options o
                            WHERE o.host_id = h.host_id AND o.scope_id = ' . self::HOST_SCOPE . '
                              AND o.code = ' . self::OPT_NTP . '
                        ) AS has_ntp
                 FROM hosts h
                 LEFT JOIN app_reservation_meta m ON m.host_id = h.host_id
                 WHERE h.dhcp_identifier = ?
                   AND h.dhcp_identifier_type = ' . self::HW_ADDR_TYPE . '
                 ORDER BY h.dhcp4_subnet_id, h.ipv4_address'
            );
            $stmt->execute([$binary]);
        } else {
            $like = '%' . $term . '%';
            $stmt = $db->prepare(
                'SELECT h.host_id,
                        HEX(h.dhcp_identifier) AS mac_hex,
                        h.ipv4_address,
                        h.dhcp4_subnet_id,
                        h.hostname,
                        m.description,
                        m.short_lease,
                        EXISTS (
                            SELECT 1 FROM dhcp4_options o
                            WHERE o.host_id = h.host_id AND o.scope_id = ' . self::HOST_SCOPE . '
                              AND o.formatted_value IS NOT NULL AND o.formatted_value != \'\'
                        ) AS has_options,
                        EXISTS (
                            SELECT 1 FROM dhcp4_options o
                            WHERE o.host_id = h.host_id AND o.scope_id = ' . self::HOST_SCOPE . '
                              AND o.code = ' . self::OPT_NTP . '
                        ) AS has_ntp
                 FROM hosts h
                 LEFT JOIN app_reservation_meta m ON m.host_id = h.host_id
                 WHERE h.dhcp_identifier_type = ' . self::HW_ADDR_TYPE . '
                   AND (h.hostname LIKE ? OR m.description LIKE ?)
                 ORDER BY h.dhcp4_subnet_id, h.ipv4_address'
            );
            $stmt->execute([$like, $like]);
        }

        $rows    = $stmt->fetchAll();
        $subnets = [];
        foreach ($rows as &$row) {
            $row['mac']         = formatMacHex($row['mac_hex']);
            $row['ip_address']  = intToIp((int)$row['ipv4_address']);
            $row['short_lease'] = (int)($row['short_lease'] ?? 0);
            $keaId = (int)$row['dhcp4_subnet_id'];
            if (!isset($subnets[$keaId])) {
                $subnets[$keaId] = Subnet::getByKeaSubnetId($keaId);
            }
            $s = $subnets[$keaId];
            $row['subnet_name']   = $s['name']   ?? '—';
            $row['subnet_app_id'] = $s['id']      ?? null;
        }
        return $rows;
    }

    /**
     * Valide une réservation. Retourne un tableau d'erreurs (vide si OK).
     * $excludeHostId : host_id à exclure lors de l'édition.
     * $subnetContext : tableau subnet complet (pour vérifier short_lease_pool_start).
     */
    public static function validate(int $keaSubnetId, array $data, ?int $excludeHostId = null, ?array $subnetContext = null): array {
        $errors = [];
        $mac    = trim($data['mac'] ?? '');
        $ip     = trim($data['ip'] ?? '');
        $shortLease     = !empty($data['short_lease']);
        $hasShortLeasePool = !empty($subnetContext['short_lease_pool_start']);

        if (empty($mac)) {
            $errors[] = 'L\'adresse MAC est obligatoire.';
        } elseif (!isValidMac($mac)) {
            $errors[] = 'Format MAC invalide (ex: bc:dd:c2:47:3d:b6).';
        }

        if (empty($ip)) {
            // IP optionnelle si short_lease activé ET un pool short-lease est défini sur le subnet
            if (!$shortLease || !$hasShortLeasePool) {
                if (!$shortLease) {
                    $errors[] = 'L\'adresse IP est obligatoire.';
                } else {
                    $errors[] = 'L\'adresse IP est obligatoire (aucun pool short-lease défini sur ce subnet).';
                }
            }
        } elseif (!isValidIp($ip)) {
            $errors[] = 'Format IP invalide.';
        }

        if (empty($data['hostname'])) {
            $errors[] = 'Le hostname est obligatoire.';
        } elseif (!preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$/', trim($data['hostname']))) {
            $errors[] = 'Hostname invalide (lettres, chiffres, tirets uniquement).';
        }

        // Vérifications unicité + pool uniquement si les formats sont valides
        if (!empty($mac) && isValidMac($mac)) {
            $errors = array_merge($errors, self::checkDuplicateMac($keaSubnetId, $mac, $excludeHostId));
        }
        // Unicité IP uniquement si une IP est fournie (vide = allocation dynamique pool short-lease)
        if (!empty($ip) && isValidIp($ip)) {
            $errors = array_merge($errors, self::checkDuplicateIp($keaSubnetId, $ip, $excludeHostId));
            $errors = array_merge($errors, self::checkPoolConflict($keaSubnetId, $ip));
        }

        // Gateway spécifique (optionnel)
        $gateway = trim($data['gateway'] ?? '');
        if ($gateway !== '' && !isValidIp($gateway)) {
            $errors[] = 'Gateway spécifique : format IPv4 invalide.';
        }

        // Routes statiques (option 121) — optionnel
        foreach ($data['routes'] ?? [] as $i => $route) {
            $dest = trim($route['dest'] ?? '');
            $rgw  = trim($route['gw']   ?? '');
            if ($dest === '' && $rgw === '') continue; // ligne vide ignorée
            if (!self::isValidCidr($dest)) {
                $errors[] = 'Route ' . ($i + 1) . ' : destination CIDR invalide (' . h($dest) . ').';
            }
            if (!isValidIp($rgw)) {
                $errors[] = 'Route ' . ($i + 1) . ' : gateway invalide (' . h($rgw) . ').';
            }
        }

        return $errors;
    }

    // ── Options DHCP hôte (scope_id = 4) ──────────────────────────────────────

    /**
     * Retourne les options DHCP de l'hôte indexées par code.
     * [code => formatted_value]
     * Cas spécial : si code=3 et cancelled=1, la clé '_no_gateway' est ajoutée.
     */
    private static function loadHostOptions(int $hostId): array {
        $stmt = Database::get()->prepare(
            'SELECT code, formatted_value FROM dhcp4_options
             WHERE host_id = ? AND scope_id = ?
               AND formatted_value IS NOT NULL AND formatted_value != \'\''
        );
        $stmt->execute([$hostId, self::HOST_SCOPE]);
        $result = [];
        foreach ($stmt->fetchAll() as $row) {
            $code = (int)$row['code'];
            if ($code === self::OPT_ROUTERS && $row['formatted_value'] === self::NO_GATEWAY_VALUE) {
                $result['_no_gateway'] = true;
            } else {
                $result[$code] = $row['formatted_value'];
            }
        }
        return $result;
    }

    /**
     * Enregistre les options DHCP d'un hôte (remplace les existantes).
     * Routes format : [['dest' => 'x.x.x.x/n', 'gw' => 'x.x.x.x'], ...]
     * $noGateway : si true, insère l'option 3 avec cancelled=1 (never-send) au lieu d'une valeur.
     */
    private static function saveHostOptions(int $hostId, string $gateway, array $routes, string $ntp = '', bool $noGateway = false): void {
        $db = Database::get();

        // Supprimer les options existantes de cet hôte
        $db->prepare('DELETE FROM dhcp4_options WHERE host_id = ? AND scope_id = ?')
           ->execute([$hostId, self::HOST_SCOPE]);

        // Gateway : soit supprimée (cancelled=1), soit spécifique, soit rien
        $gw = $noGateway ? self::NO_GATEWAY_VALUE : trim($gateway);
        if ($gw !== '') {
            $db->prepare(
                'INSERT INTO dhcp4_options
                 (code, formatted_value, space, persistent, host_id, scope_id, modification_ts)
                 VALUES (?, ?, \'dhcp4\', 1, ?, ?, NOW())'
            )->execute([self::OPT_ROUTERS, $gw, $hostId, self::HOST_SCOPE]);
        }

        // Routes statiques (option 121 — classless-static-routes)
        // Format Kea : "10.0.0.0/8 - 192.168.1.254, 172.16.0.0/12 - 192.168.1.254"
        $validRoutes = [];
        foreach ($routes as $r) {
            $dest = trim($r['dest'] ?? '');
            $rgw  = trim($r['gw']   ?? '');
            if ($dest !== '' && $rgw !== '') {
                $validRoutes[] = $dest . ' - ' . $rgw;
            }
        }
        if ($validRoutes) {
            $db->prepare(
                'INSERT INTO dhcp4_options
                 (code, formatted_value, space, persistent, host_id, scope_id, modification_ts)
                 VALUES (?, ?, \'dhcp4\', 1, ?, ?, NOW())'
            )->execute([self::OPT_ROUTES, implode(', ', $validRoutes), $hostId, self::HOST_SCOPE]);
        }

        // NTP server (option 42 — ntp-servers)
        $ntpVal = trim($ntp);
        if ($ntpVal !== '') {
            $db->prepare(
                'INSERT INTO dhcp4_options
                 (code, formatted_value, space, persistent, host_id, scope_id, modification_ts)
                 VALUES (?, ?, \'dhcp4\', 1, ?, ?, NOW())'
            )->execute([self::OPT_NTP, $ntpVal, $hostId, self::HOST_SCOPE]);
        }
    }

    /**
     * Parse "10.0.0.0/8 192.168.1.254, 172.16.0.0/12 192.168.1.254"
     * → [['dest' => '10.0.0.0/8', 'gw' => '192.168.1.254'], ...]
     */
    private static function parseRoutes(string $value): array {
        $routes = [];
        foreach (explode(',', $value) as $part) {
            $part = trim($part);
            if ($part === '') continue;
            // Format Kea : "10.0.0.0/8 - 192.168.1.254"
            if (strpos($part, ' - ') !== false) {
                $pieces = explode(' - ', $part, 2);
            } else {
                // Compatibilité ascendante : ancien format sans tiret
                $pieces = preg_split('/\s+/', $part, 2);
            }
            if (count($pieces) === 2) {
                $routes[] = ['dest' => trim($pieces[0]), 'gw' => trim($pieces[1])];
            }
        }
        return $routes;
    }

    private static function isValidCidr(string $cidr): bool {
        if (!preg_match('/^(\d{1,3}(?:\.\d{1,3}){3})\/(\d{1,2})$/', $cidr, $m)) {
            return false;
        }
        if (!filter_var($m[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            return false;
        }
        return (int)$m[2] >= 0 && (int)$m[2] <= 32;
    }

    // ── Vérifications unicité / pool ───────────────────────────────────────────

    private static function checkDuplicateMac(int $subnetId, string $mac, ?int $excludeHostId): array {
        $binary = macToBinary($mac);
        $sql    = 'SELECT host_id FROM hosts WHERE dhcp4_subnet_id = ? AND dhcp_identifier = ?' .
                  ($excludeHostId ? ' AND host_id <> ' . (int)$excludeHostId : '');
        $stmt   = Database::get()->prepare($sql);
        $stmt->execute([$subnetId, $binary]);
        return $stmt->fetch() ? ['Ce MAC est déjà réservé sur ce subnet.'] : [];
    }

    private static function checkPoolConflict(int $keaSubnetId, string $ip): array {
        $subnet = Subnet::getByKeaSubnetId($keaSubnetId);
        if (!$subnet || !$subnet['has_dynamic_pool'] || !$subnet['range_start'] || !$subnet['range_end']) {
            return [];
        }
        $ipInt    = ipToInt($ip);
        $startInt = ipToInt($subnet['range_start']);
        $endInt   = ipToInt($subnet['range_end']);
        if ($ipInt >= $startInt && $ipInt <= $endInt) {
            return [
                'L\'IP ' . $ip . ' appartient au pool dynamique ('
                . $subnet['range_start'] . ' – ' . $subnet['range_end'] . ').'
                . ' Choisissez une IP hors de cette plage.',
            ];
        }
        return [];
    }

    private static function checkDuplicateIp(int $subnetId, string $ip, ?int $excludeHostId): array {
        $ipInt = ipToInt($ip);
        $sql   = 'SELECT host_id FROM hosts WHERE dhcp4_subnet_id = ? AND ipv4_address = ?' .
                 ($excludeHostId ? ' AND host_id <> ' . (int)$excludeHostId : '');
        $stmt  = Database::get()->prepare($sql);
        $stmt->execute([$subnetId, $ipInt]);
        return $stmt->fetch() ? ['Cette IP est déjà réservée sur ce subnet.'] : [];
    }
}
