<?php

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

final class SPP_Repository
{
    /**
     * @return array<int, array<string, mixed>>
     */
    public function templates(): array
    {
        global $wpdb;

        $table = SPP_Activator::table('templates');
        $rows = $wpdb->get_results("SELECT * FROM {$table} WHERE is_active = 1 ORDER BY name ASC", ARRAY_A);

        return array_map([$this, 'template_dto'], is_array($rows) ? $rows : []);
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function admin_templates(): array
    {
        global $wpdb;

        $table = SPP_Activator::table('templates');
        $rows = $wpdb->get_results("SELECT * FROM {$table} ORDER BY is_active DESC, name ASC", ARRAY_A);

        return array_map([$this, 'template_dto'], is_array($rows) ? $rows : []);
    }

    /**
     * @return array<string, mixed>|null
     */
    public function template(int $id): ?array
    {
        global $wpdb;

        $table = SPP_Activator::table('templates');
        $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d AND is_active = 1", $id), ARRAY_A);

        return is_array($row) ? $row : null;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function deployments(int $actor_id, bool $include_all = false): array
    {
        global $wpdb;

        $deployments = SPP_Activator::table('deployments');
        $templates = SPP_Activator::table('templates');
        $shares = SPP_Activator::table('deployment_shares');
        $users = $wpdb->users;

        $sql = "SELECT DISTINCT d.*, t.name AS template_name, t.cpu_cores, t.memory_mb, t.disk_gb, u.display_name AS requested_by_name,
                    CASE
                        WHEN d.requested_by = %d THEN 'owner'
                        WHEN s.user_id IS NOT NULL THEN 'shared'
                        ELSE 'admin'
                    END AS access_type
             FROM {$deployments} d
             INNER JOIN {$templates} t ON t.id = d.template_id
             INNER JOIN {$users} u ON u.ID = d.requested_by
             LEFT JOIN {$shares} s ON s.deployment_id = d.id AND s.user_id = %d
             WHERE d.status <> 'DELETED'";
        $params = [$actor_id, $actor_id];

        if (!$include_all) {
            $sql .= ' AND (d.requested_by = %d OR s.user_id IS NOT NULL)';
            $params[] = $actor_id;
        }

        $sql .= ' ORDER BY d.created_at DESC';
        $rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);

        return array_map([$this, 'deployment_summary_dto'], is_array($rows) ? $rows : []);
    }

    /**
     * @return array<string, mixed>|null
     */
    public function deployment(int $id): ?array
    {
        global $wpdb;

        $deployments = SPP_Activator::table('deployments');
        $templates = SPP_Activator::table('templates');
        $users = $wpdb->users;

        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT d.*, t.name AS template_name, t.cpu_cores, t.memory_mb, t.disk_gb, u.display_name AS requested_by_name
                 FROM {$deployments} d
                 INNER JOIN {$templates} t ON t.id = d.template_id
                 INNER JOIN {$users} u ON u.ID = d.requested_by
                 WHERE d.id = %d",
                $id
            ),
            ARRAY_A
        );

        return is_array($row) ? $this->deployment_detail_dto($row) : null;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function deployment_for_user(int $id, int $actor_id, bool $include_all = false): ?array
    {
        global $wpdb;

        $deployments = SPP_Activator::table('deployments');
        $templates = SPP_Activator::table('templates');
        $shares = SPP_Activator::table('deployment_shares');
        $users = $wpdb->users;

        $sql = "SELECT d.*, t.name AS template_name, t.cpu_cores, t.memory_mb, t.disk_gb, u.display_name AS requested_by_name,
                    CASE
                        WHEN d.requested_by = %d THEN 'owner'
                        WHEN s.user_id IS NOT NULL THEN 'shared'
                        ELSE 'admin'
                    END AS access_type
                 FROM {$deployments} d
                 INNER JOIN {$templates} t ON t.id = d.template_id
                 INNER JOIN {$users} u ON u.ID = d.requested_by
                 LEFT JOIN {$shares} s ON s.deployment_id = d.id AND s.user_id = %d
                 WHERE d.id = %d AND d.status <> 'DELETED'";
        $params = [$actor_id, $actor_id, $id];

        if (!$include_all) {
            $sql .= ' AND (d.requested_by = %d OR s.user_id IS NOT NULL)';
            $params[] = $actor_id;
        }

        $row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A);

        return is_array($row) ? $this->deployment_detail_dto($row) : null;
    }

    /**
     * @param array<string, mixed> $template
     * @return array<string, mixed>
     */
    public function create_deployment(array $template, string $name, ?int $ttl_hours, int $vm_id, array $ip_addresses, int $actor_id): array
    {
        global $wpdb;

        $now = current_time('mysql');
        $expires_at = $ttl_hours === null ? null : date('Y-m-d H:i:s', current_time('timestamp') + ($ttl_hours * HOUR_IN_SECONDS));
        $table = SPP_Activator::table('deployments');

        $wpdb->insert($table, [
            'name' => $name,
            'status' => 'STOPPED',
            'provisioning_type' => $this->normalise_template_type((string) ($template['provisioning_type'] ?? 'qemu')),
            'proxmox_vm_id' => $vm_id,
            'ip_addresses' => wp_json_encode(array_values($ip_addresses)),
            'expires_at' => $expires_at,
            'created_at' => $now,
            'updated_at' => $now,
            'template_id' => (int) $template['id'],
            'requested_by' => $actor_id,
        ]);

        $deployment_id = (int) $wpdb->insert_id;
        $this->audit('DEPLOYMENT_CREATED', 'deployment', $deployment_id, $actor_id, [
            'template_id' => (int) $template['id'],
            'provisioning_type' => $this->normalise_template_type((string) ($template['provisioning_type'] ?? 'qemu')),
            'proxmox_vm_id' => $vm_id,
            'ip_addresses' => $ip_addresses,
            'ttl_hours' => $ttl_hours,
            'never_expire' => $ttl_hours === null,
        ]);

        return $this->deployment($deployment_id);
    }

    public function update_deployment_status(int $id, string $status, string $action, int $actor_id): ?array
    {
        global $wpdb;

        $table = SPP_Activator::table('deployments');
        $wpdb->update($table, [
            'status' => $status,
            'updated_at' => current_time('mysql'),
        ], ['id' => $id]);

        if ($status === 'DELETED') {
            $wpdb->delete(SPP_Activator::table('deployment_shares'), ['deployment_id' => $id]);
        }

        $this->audit($action, 'deployment', $id, $actor_id, ['status' => $status]);

        return $this->deployment($id);
    }

    public function update_deployment_status_and_ips(int $id, string $status, array $ip_addresses, string $action, int $actor_id): ?array
    {
        global $wpdb;

        $table = SPP_Activator::table('deployments');
        $wpdb->update($table, [
            'status' => $status,
            'ip_addresses' => wp_json_encode(array_values($ip_addresses)),
            'updated_at' => current_time('mysql'),
        ], ['id' => $id]);

        $this->audit($action, 'deployment', $id, $actor_id, [
            'status' => $status,
            'ip_addresses' => $ip_addresses,
        ]);

        return $this->deployment($id);
    }

    public function update_deployment_ips(int $id, array $ip_addresses, int $actor_id): ?array
    {
        global $wpdb;

        $table = SPP_Activator::table('deployments');
        $wpdb->update($table, [
            'ip_addresses' => wp_json_encode(array_values($ip_addresses)),
            'updated_at' => current_time('mysql'),
        ], ['id' => $id]);

        $this->audit('DEPLOYMENT_IPS_REFRESHED', 'deployment', $id, $actor_id, [
            'ip_addresses' => $ip_addresses,
        ]);

        return $this->deployment($id);
    }

    public function prolong_deployment(int $id, ?int $ttl_hours, int $actor_id): ?array
    {
        global $wpdb;

        $expires_at = $ttl_hours === null ? null : date('Y-m-d H:i:s', current_time('timestamp') + ($ttl_hours * HOUR_IN_SECONDS));
        $table = SPP_Activator::table('deployments');

        $wpdb->update($table, [
            'status' => 'STOPPED',
            'expires_at' => $expires_at,
            'error_message' => null,
            'updated_at' => current_time('mysql'),
        ], ['id' => $id]);

        $this->audit('DEPLOYMENT_PROLONGED', 'deployment', $id, $actor_id, [
            'ttl_hours' => $ttl_hours,
            'never_expire' => $ttl_hours === null,
        ]);

        return $this->deployment($id);
    }

    /**
     * @return array<string, mixed>|null
     */
    public function deployment_record(int $id): ?array
    {
        global $wpdb;

        $table = SPP_Activator::table('deployments');
        $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A);

        return is_array($row) ? $row : null;
    }

    public function user_can_access_deployment(int $deployment_id, int $actor_id, bool $include_all = false): bool
    {
        if ($include_all) {
            return $this->deployment_record($deployment_id) !== null;
        }

        return $this->user_owns_deployment($deployment_id, $actor_id)
            || $this->deployment_is_shared_with_user($deployment_id, $actor_id);
    }

    public function user_owns_deployment(int $deployment_id, int $actor_id): bool
    {
        global $wpdb;

        $table = SPP_Activator::table('deployments');

        return (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE id = %d AND requested_by = %d",
                $deployment_id,
                $actor_id
            )
        ) > 0;
    }

    private function deployment_is_shared_with_user(int $deployment_id, int $actor_id): bool
    {
        global $wpdb;

        $table = SPP_Activator::table('deployment_shares');

        return (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE deployment_id = %d AND user_id = %d",
                $deployment_id,
                $actor_id
            )
        ) > 0;
    }

    /**
     * @return array<int, array{id:int,displayName:string,userLogin:string,userEmail:string,sharedAt:string}>
     */
    public function deployment_shares(int $deployment_id): array
    {
        global $wpdb;

        $shares = SPP_Activator::table('deployment_shares');
        $users = $wpdb->users;
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT u.ID AS id, u.display_name, u.user_login, u.user_email, s.created_at
                 FROM {$shares} s
                 INNER JOIN {$users} u ON u.ID = s.user_id
                 WHERE s.deployment_id = %d
                 ORDER BY u.display_name ASC, u.user_login ASC",
                $deployment_id
            ),
            ARRAY_A
        );

        return array_map(static function (array $row): array {
            return [
                'id' => (int) $row['id'],
                'displayName' => (string) $row['display_name'],
                'userLogin' => (string) $row['user_login'],
                'userEmail' => (string) $row['user_email'],
                'sharedAt' => (string) $row['created_at'],
            ];
        }, is_array($rows) ? $rows : []);
    }

    public function share_deployment(int $deployment_id, int $target_user_id, int $actor_id): void
    {
        global $wpdb;

        $record = $this->deployment_record($deployment_id);
        if ($record === null || (int) $record['requested_by'] === $target_user_id) {
            return;
        }

        $table = SPP_Activator::table('deployment_shares');
        $wpdb->replace($table, [
            'deployment_id' => $deployment_id,
            'user_id' => $target_user_id,
            'created_by' => $actor_id,
            'created_at' => current_time('mysql'),
        ]);

        $this->audit('DEPLOYMENT_SHARED', 'deployment', $deployment_id, $actor_id, [
            'shared_with_user_id' => $target_user_id,
        ]);
    }

    public function unshare_deployment(int $deployment_id, int $target_user_id, int $actor_id): void
    {
        global $wpdb;

        $table = SPP_Activator::table('deployment_shares');
        $wpdb->delete($table, [
            'deployment_id' => $deployment_id,
            'user_id' => $target_user_id,
        ]);

        $this->audit('DEPLOYMENT_UNSHARED', 'deployment', $deployment_id, $actor_id, [
            'unshared_user_id' => $target_user_id,
        ]);
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function due_for_expiration(): array
    {
        global $wpdb;

        $table = SPP_Activator::table('deployments');
        $now = current_time('mysql');
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$table}
                 WHERE expires_at IS NOT NULL
                   AND expires_at <= %s
                   AND status IN ('PROVISIONING', 'STOPPED', 'RUNNING')",
                $now
            ),
            ARRAY_A
        );

        return is_array($rows) ? $rows : [];
    }

    public function mark_deployment_expired(int $id, ?string $stop_error): void
    {
        global $wpdb;

        $table = SPP_Activator::table('deployments');
        $wpdb->update($table, [
            'status' => 'EXPIRED',
            'error_message' => $stop_error,
            'updated_at' => current_time('mysql'),
        ], ['id' => $id]);

        $this->audit('DEPLOYMENT_EXPIRED', 'deployment', $id, 0, [
            'stopped' => $stop_error === null,
            'stop_error' => $stop_error,
        ]);
    }

    /**
     * @return array{userUsedMb:int,userLimitMb:int,globalUsedMb:int,globalLimitMb:int}
     */
    public function quota(int $actor_id): array
    {
        return [
            'userUsedMb' => $this->allocated_memory_mb($actor_id),
            'userLimitMb' => $this->user_memory_limit_mb($actor_id),
            'globalUsedMb' => $this->allocated_memory_mb(null),
            'globalLimitMb' => max(0, (int) get_option('spp_quota_global_memory_mb', 0)),
        ];
    }

    /**
     * @param array<string, mixed> $data
     */
    public function upsert_template(array $data, int $actor_id): ?array
    {
        global $wpdb;

        $table = SPP_Activator::table('templates');
        $now = current_time('mysql');
        $provisioning_type = $this->normalise_template_type((string) ($data['provisioning_type'] ?? 'qemu'));
        $proxmox_template_id = $provisioning_type === 'qemu' ? absint($data['proxmox_template_id']) : 0;
        $proxmox_template_ref = $provisioning_type === 'lxc' ? sanitize_text_field((string) ($data['proxmox_template_ref'] ?? '')) : null;
        $template_key = $this->unique_template_key((string) $data['template_key'], $provisioning_type, $proxmox_template_id, (string) $proxmox_template_ref);
        $existing_id = $this->template_existing_id($provisioning_type, $proxmox_template_id, (string) $proxmox_template_ref, $template_key);

        $row = [
            'template_key' => $template_key,
            'name' => sanitize_text_field((string) $data['name']),
            'description' => sanitize_textarea_field((string) $data['description']),
            'os_type' => strtoupper(sanitize_key((string) $data['os_type'])),
            'cpu_cores' => max(1, absint($data['cpu_cores'])),
            'memory_mb' => max(128, absint($data['memory_mb'])),
            'disk_gb' => max(1, absint($data['disk_gb'])),
            'default_ttl_hours' => max(1, min(720, absint($data['default_ttl_hours']))),
            'provisioning_type' => $provisioning_type,
            'proxmox_template_id' => $proxmox_template_id,
            'proxmox_template_ref' => $proxmox_template_ref,
            'is_active' => 1,
            'updated_at' => $now,
        ];

        if ($existing_id > 0) {
            $wpdb->update($table, $row, ['id' => $existing_id]);
            $template_id = $existing_id;
            $action = 'TEMPLATE_UPDATED';
        } else {
            $wpdb->insert($table, array_merge($row, ['created_at' => $now]));
            $template_id = (int) $wpdb->insert_id;
            $action = 'TEMPLATE_IMPORTED';
        }

        $this->audit($action, 'template', $template_id, $actor_id, [
            'provisioning_type' => (string) $row['provisioning_type'],
            'proxmox_template_id' => (int) $row['proxmox_template_id'],
            'proxmox_template_ref' => (string) ($row['proxmox_template_ref'] ?? ''),
            'name' => (string) $row['name'],
        ]);

        return $this->template_for_admin($template_id);
    }

    /**
     * @param array<string, mixed> $data
     */
    public function update_template(int $id, array $data, int $actor_id): ?array
    {
        global $wpdb;

        $table = SPP_Activator::table('templates');
        $provisioning_type = $this->normalise_template_type((string) ($data['provisioning_type'] ?? 'qemu'));
        $proxmox_template_id = $provisioning_type === 'qemu' ? absint($data['proxmox_template_id']) : 0;
        $proxmox_template_ref = $provisioning_type === 'lxc' ? sanitize_text_field((string) ($data['proxmox_template_ref'] ?? '')) : null;
        $row = [
            'name' => sanitize_text_field((string) $data['name']),
            'description' => sanitize_textarea_field((string) $data['description']),
            'os_type' => strtoupper(sanitize_key((string) $data['os_type'])),
            'cpu_cores' => max(1, absint($data['cpu_cores'])),
            'memory_mb' => max(128, absint($data['memory_mb'])),
            'disk_gb' => max(1, absint($data['disk_gb'])),
            'default_ttl_hours' => max(1, min(720, absint($data['default_ttl_hours']))),
            'provisioning_type' => $provisioning_type,
            'proxmox_template_id' => $proxmox_template_id,
            'proxmox_template_ref' => $proxmox_template_ref,
            'is_active' => empty($data['is_active']) ? 0 : 1,
            'updated_at' => current_time('mysql'),
        ];

        $wpdb->update($table, $row, ['id' => $id]);

        $this->audit('TEMPLATE_UPDATED', 'template', $id, $actor_id, [
            'provisioning_type' => (string) $row['provisioning_type'],
            'proxmox_template_id' => (int) $row['proxmox_template_id'],
            'proxmox_template_ref' => (string) ($row['proxmox_template_ref'] ?? ''),
            'name' => (string) $row['name'],
            'is_active' => (int) $row['is_active'],
        ]);

        return $this->template_for_admin($id);
    }

    public function deactivate_template(int $id, int $actor_id): ?array
    {
        global $wpdb;

        $table = SPP_Activator::table('templates');
        $wpdb->update($table, [
            'is_active' => 0,
            'updated_at' => current_time('mysql'),
        ], ['id' => $id]);

        $this->audit('TEMPLATE_REMOVED', 'template', $id, $actor_id, []);

        return $this->template_for_admin($id);
    }

    /**
     * @return array<int, string>
     */
    public function active_template_identity_keys(): array
    {
        global $wpdb;

        $table = SPP_Activator::table('templates');
        $rows = $wpdb->get_results(
            "SELECT provisioning_type, proxmox_template_id, proxmox_template_ref FROM {$table} WHERE is_active = 1",
            ARRAY_A
        );
        $keys = [];

        foreach (is_array($rows) ? $rows : [] as $row) {
            $keys[] = $this->template_identity_key(
                (string) ($row['provisioning_type'] ?? 'qemu'),
                (int) ($row['proxmox_template_id'] ?? 0),
                (string) ($row['proxmox_template_ref'] ?? '')
            );
        }

        return array_values(array_unique($keys));
    }

    /**
     * @param array<int, string> $permissions
     */
    public function update_user_access(int $user_id, array $permissions, ?int $memory_quota_mb, int $actor_id): void
    {
        if ($user_id < 1) {
            return;
        }

        $permissions = SPP_Permissions::sanitize_permissions($permissions);

        if (empty($permissions)) {
            delete_user_meta($user_id, SPP_Permissions::META_KEY);
        } else {
            update_user_meta($user_id, SPP_Permissions::META_KEY, $permissions);
        }

        if ($memory_quota_mb === null) {
            delete_user_meta($user_id, 'spp_memory_quota_mb');
        } else {
            update_user_meta($user_id, 'spp_memory_quota_mb', max(0, $memory_quota_mb));
        }

        $this->audit('USER_ACCESS_UPDATED', 'user', $user_id, $actor_id, [
            'permissions' => $permissions,
            'memory_quota_mb' => $memory_quota_mb,
        ]);
    }

    private function user_memory_limit_mb(int $actor_id): int
    {
        $user_limit = get_user_meta($actor_id, 'spp_memory_quota_mb', true);

        if ($user_limit !== '') {
            return max(0, (int) $user_limit);
        }

        return max(0, (int) get_option('spp_quota_user_memory_mb', 0));
    }

    private function allocated_memory_mb(?int $actor_id): int
    {
        global $wpdb;

        $deployments = SPP_Activator::table('deployments');
        $templates = SPP_Activator::table('templates');
        $active_statuses = ['PROVISIONING', 'STOPPED', 'RUNNING', 'DELETING'];
        $placeholders = implode(',', array_fill(0, count($active_statuses), '%s'));

        $sql = "SELECT COALESCE(SUM(t.memory_mb), 0)
                FROM {$deployments} d
                INNER JOIN {$templates} t ON t.id = d.template_id
                WHERE d.status IN ({$placeholders})";
        $params = $active_statuses;

        if ($actor_id !== null) {
            $sql .= ' AND d.requested_by = %d';
            $params[] = $actor_id;
        }

        return (int) $wpdb->get_var($wpdb->prepare($sql, $params));
    }

    /**
     * @return array<string, mixed>|null
     */
    private function template_for_admin(int $id): ?array
    {
        global $wpdb;

        $table = SPP_Activator::table('templates');
        $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A);

        return is_array($row) ? $this->template_dto($row) : null;
    }

    private function unique_template_key(string $raw_key, string $provisioning_type, int $proxmox_template_id, string $proxmox_template_ref): string
    {
        $key = sanitize_title($raw_key);

        if ($key === '') {
            $key = $this->template_identity_key($provisioning_type, $proxmox_template_id, $proxmox_template_ref);
        }

        return substr($key, 0, 80);
    }

    private function template_existing_id(string $provisioning_type, int $proxmox_template_id, string $proxmox_template_ref, string $template_key): int
    {
        global $wpdb;

        $table = SPP_Activator::table('templates');

        if ($provisioning_type === 'lxc') {
            return (int) $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT id FROM {$table}
                     WHERE (provisioning_type = 'lxc' AND proxmox_template_ref = %s) OR template_key = %s
                     LIMIT 1",
                    $proxmox_template_ref,
                    $template_key
                )
            );
        }

        return (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM {$table}
                 WHERE (provisioning_type = 'qemu' AND proxmox_template_id = %d) OR template_key = %s
                 LIMIT 1",
                $proxmox_template_id,
                $template_key
            )
        );
    }

    private function template_identity_key(string $provisioning_type, int $proxmox_template_id, string $proxmox_template_ref): string
    {
        return $this->normalise_template_type($provisioning_type) === 'lxc'
            ? 'lxc:' . $proxmox_template_ref
            : 'qemu:' . $proxmox_template_id;
    }

    private function normalise_template_type(string $type): string
    {
        return strtolower($type) === 'lxc' ? 'lxc' : 'qemu';
    }

    /**
     * @param array<string, mixed> $metadata
     */
    private function audit(string $action, string $entity_type, int $entity_id, int $actor_id, array $metadata): void
    {
        global $wpdb;

        $wpdb->insert(SPP_Activator::table('audit_logs'), [
            'action' => $action,
            'entity_type' => $entity_type,
            'entity_id' => $entity_id,
            'actor_id' => $actor_id,
            'metadata' => wp_json_encode($metadata),
            'created_at' => current_time('mysql'),
        ]);
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>
     */
    private function template_dto(array $row): array
    {
        return [
            'id' => (int) $row['id'],
            'key' => (string) $row['template_key'],
            'name' => (string) $row['name'],
            'description' => (string) $row['description'],
            'osType' => (string) $row['os_type'],
            'cpuCores' => (int) $row['cpu_cores'],
            'memoryMb' => (int) $row['memory_mb'],
            'diskGb' => (int) $row['disk_gb'],
            'defaultTtlHours' => (int) $row['default_ttl_hours'],
            'provisioningType' => $this->normalise_template_type((string) ($row['provisioning_type'] ?? 'qemu')),
            'proxmoxTemplateId' => (int) $row['proxmox_template_id'],
            'proxmoxTemplateRef' => (string) ($row['proxmox_template_ref'] ?? ''),
            'isActive' => (int) $row['is_active'] === 1,
        ];
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>
     */
    private function deployment_summary_dto(array $row): array
    {
        return [
            'id' => (int) $row['id'],
            'name' => (string) $row['name'],
            'status' => (string) $row['status'],
            'provisioningType' => $this->normalise_template_type((string) ($row['provisioning_type'] ?? 'qemu')),
            'templateName' => (string) $row['template_name'],
            'requestedById' => (int) $row['requested_by'],
            'requestedByName' => (string) $row['requested_by_name'],
            'accessType' => isset($row['access_type']) ? (string) $row['access_type'] : null,
            'ipAddresses' => $this->ip_addresses_from_row($row),
            'expiresAt' => $row['expires_at'] === null ? null : (string) $row['expires_at'],
            'createdAt' => (string) $row['created_at'],
        ];
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>
     */
    private function deployment_detail_dto(array $row): array
    {
        return array_merge($this->deployment_summary_dto($row), [
            'templateId' => (int) $row['template_id'],
            'proxmoxVmId' => isset($row['proxmox_vm_id']) ? (int) $row['proxmox_vm_id'] : null,
            'proxmoxResourceType' => $this->normalise_template_type((string) ($row['provisioning_type'] ?? 'qemu')),
            'proxmoxResourceId' => isset($row['proxmox_vm_id']) ? (int) $row['proxmox_vm_id'] : null,
            'cpuCores' => (int) $row['cpu_cores'],
            'memoryMb' => (int) $row['memory_mb'],
            'diskGb' => (int) $row['disk_gb'],
            'errorMessage' => $row['error_message'] === null ? null : (string) $row['error_message'],
        ]);
    }

    /**
     * @param array<string, mixed> $row
     * @return array<int, string>
     */
    private function ip_addresses_from_row(array $row): array
    {
        if (empty($row['ip_addresses'])) {
            return [];
        }

        $decoded = json_decode((string) $row['ip_addresses'], true);

        return is_array($decoded) ? array_values(array_map('strval', $decoded)) : [];
    }
}
