<?php
/**
 * Docker API Integration Library
 * 
 * Functions for interacting with Docker API to create and manage containers
 */

// Docker socket path (typically /var/run/docker.sock)
define('DOCKER_SOCKET', '/var/run/docker.sock');

/**
 * Docker API Class
 * 
 * Object-oriented wrapper for Docker API operations
 */
class docker_api {
    
    /**
     * List all containers
     * 
     * @param bool $include_stopped Include stopped containers
     * @return array List of containers
     */
    public function list_containers($include_stopped = false) {
        $endpoint = '/containers/json';
        if ($include_stopped) {
            $endpoint .= '?all=true';
        }
        
        $result = dockerApiRequest('GET', $endpoint);
        if ($result['success']) {
            return $result['data'] ?: [];
        } else {
            error_log('Docker API Error (list_containers): ' . $result['error']);
            return [];
        }
    }
    
    /**
     * Restart a container
     * 
     * @param string $container_id Container ID
     * @return array Result array with success flag
     */
    public function restart_container($container_id) {
        $result = dockerApiRequest('POST', "/containers/{$container_id}/restart");
        return $result;
    }
    
    /**
     * Stop a container
     * 
     * @param string $container_id Container ID
     * @return array Result array with success flag
     */
    public function stop_container($container_id) {
        $result = dockerApiRequest('POST', "/containers/{$container_id}/stop");
        return $result;
    }
    
    /**
     * Inspect a container
     * 
     * @param string $container_id Container ID
     * @return array Result array with success flag and container data
     */
    public function inspect_container($container_id) {
        $result = dockerApiRequest('GET', "/containers/{$container_id}/json");
        return $result;
    }
    
    /**
     * Remove a container
     * 
     * @param string $container_id Container ID
     * @return array Result array with success flag
     */
    public function remove_container($container_id) {
        // Stop the container first if it's running
        $this->stop_container($container_id);
        
        // Then remove it
        $result = dockerApiRequest('DELETE', "/containers/{$container_id}");
        return $result;
    }
    
    /**
     * Pull an image from registry
     * 
     * @param string $image_name Image name to pull
     * @return array Result array with success flag
     */
    public function pull_image($image_name) {
        $result = dockerApiRequest('POST', "/images/create?fromImage=" . urlencode($image_name));
        
        if ($result['success']) {
            // Update last pull time in database using mysqlmgmt
            try {
                require_once('/docker/whp/web/libs/mysqlmgmt.php');
                $MySQLMgmt = new mysqlmgmt();
                $MySQLMgmt->updateImagePullTime($image_name);
            } catch (Exception $e) {
                error_log('Failed to update image pull time: ' . $e->getMessage());
            }
            
            // Trigger auto-recreation for sites with auto_recreate_on_update enabled
            $this->triggerAutoRecreation($image_name);
            
            return ['success' => true, 'message' => 'Image pulled successfully'];
        }
        
        return $result;
    }
    
    /**
     * Pull an image from registry without triggering auto-recreation
     * Used for manual container recreation where we handle the recreation ourselves
     * 
     * @param string $image_name Image name to pull
     * @return array Result array with success flag
     */
    public function pull_image_manual($image_name) {
        $result = dockerApiRequest('POST', "/images/create?fromImage=" . urlencode($image_name));
        
        if ($result['success']) {
            // Update last pull time in database using mysqlmgmt
            try {
                require_once('/docker/whp/web/libs/mysqlmgmt.php');
                $MySQLMgmt = new mysqlmgmt();
                $MySQLMgmt->updateImagePullTime($image_name);
            } catch (Exception $e) {
                error_log('Failed to update image pull time: ' . $e->getMessage());
            }
            
            // Note: We do NOT trigger auto-recreation here since this is manual
            
            return ['success' => true, 'message' => 'Image pulled successfully'];
        }
        
        return $result;
    }
    
    /**
     * Get image information
     * 
     * @param string $image_name Image name
     * @return array Result array with success flag and image data
     */
    public function inspect_image($image_name) {
        $result = dockerApiRequest('GET', "/images/" . urlencode($image_name) . "/json");
        return $result;
    }
    
    /**
     * List all images
     * 
     * @return array List of images
     */
    public function list_images() {
        $result = dockerApiRequest('GET', '/images/json');
        if ($result['success']) {
            return $result['data'] ?: [];
        } else {
            error_log('Docker API Error (list_images): ' . $result['error']);
            return [];
        }
    }
    
    /**
     * Check if an image is being used by any containers
     * 
     * @param string $image_name Image name to check
     * @return bool True if image is in use
     */
    public function is_image_in_use($image_name) {
        $containers = $this->list_containers(true); // Include stopped containers
        
        foreach ($containers as $container) {
            if ($container['Image'] === $image_name) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Remove a Docker image
     * 
     * @param string $image_name Image name to remove
     * @return array Result array with success flag
     */
    public function remove_image($image_name) {
        $result = dockerApiRequest('DELETE', "/images/" . urlencode($image_name));
        return $result;
    }
    
    /**
     * Clean up unused image after container type deletion
     * 
     * @param string $image_name Image name to clean up
     * @return array Result array with success flag and message
     */
    public function cleanup_image_if_unused($image_name) {
        if (!$this->is_image_in_use($image_name)) {
            $result = $this->remove_image($image_name);
            if ($result['success']) {
                return ['success' => true, 'message' => "Image '{$image_name}' cleaned up successfully"];
            } else {
                return ['success' => false, 'message' => "Failed to clean up image '{$image_name}': " . $result['error']];
            }
        } else {
            return ['success' => true, 'message' => "Image '{$image_name}' is still in use by other containers"];
        }
    }
    
    /**
     * Replace variables in environment configuration
     * 
     * @param array $env_config Environment configuration with placeholders
     * @param array $variables Variable values to substitute
     * @return array Processed environment array
     */
    public function process_environment_variables($env_config, $variables) {
        $processed_env = [];
        
        foreach ($env_config as $key => $value) {
            $processed_value = $value;
            
            // Replace placeholders with actual values
            foreach ($variables as $var_name => $var_value) {
                $placeholder = '${' . strtoupper($var_name) . '}';
                $processed_value = str_replace($placeholder, $var_value, $processed_value);
            }
            
            $processed_env[] = $key . '=' . $processed_value;
        }
        
        return $processed_env;
    }
    
    /**
     * Create a container
     * 
     * @param array $container_config Container configuration
     * @return array Result array with success flag and container ID
     */
    public function create_container($container_config) {
        // Process environment variables if provided
        $env_vars = [];
        if (isset($container_config['env'])) {
            if (is_array($container_config['env'])) {
                $env_vars = $container_config['env'];
            }
        }
        
        // If we have enhanced environment configuration, process it
        if (isset($container_config['startup_env']) && isset($container_config['variables'])) {
            $startup_env = json_decode($container_config['startup_env'], true);
            if ($startup_env) {
                $processed_env = $this->process_environment_variables($startup_env, $container_config['variables']);
                $env_vars = array_merge($env_vars, $processed_env);
            }
        }
        
        // Convert the simplified config to Docker API format
        $docker_config = [
            'Image' => $container_config['image'],
            'Hostname' => $container_config['name'],
            'Env' => $env_vars,
            'Labels' => $container_config['labels'] ?? [],
            'HostConfig' => [
                'RestartPolicy' => [
                    'Name' => 'unless-stopped'
                ],
                'Binds' => $container_config['volumes'] ?? [],
                'NetworkMode' => $container_config['network'] ?? 'bridge'
            ]
        ];
        
        // Set resource limits if provided
        if (isset($container_config['cpu'])) {
            $docker_config['HostConfig']['CpuPeriod'] = 100000;
            $docker_config['HostConfig']['CpuQuota'] = (int)(100000 * $container_config['cpu']);
        }
        
        if (isset($container_config['memory'])) {
            $docker_config['HostConfig']['Memory'] = $container_config['memory'] * 1024 * 1024; // Convert MB to bytes
            $docker_config['HostConfig']['MemorySwap'] = $container_config['memory'] * 1024 * 1024; // No swap
        }
        
        // Create the container with name as URL parameter
        $result = dockerApiRequest('POST', '/containers/create?name=' . urlencode($container_config['name']), $docker_config);

        // If the image isn't available locally, pull it and retry
        if (!$result['success'] && isset($result['error']) && stripos($result['error'], 'No such image') !== false) {
            error_log("Image not found locally, pulling: " . $container_config['image']);
            $pull_result = $this->pull_image_manual($container_config['image']);
            if ($pull_result['success']) {
                error_log("Image pulled successfully, retrying container creation");
                $result = dockerApiRequest('POST', '/containers/create?name=' . urlencode($container_config['name']), $docker_config);
            } else {
                return ['success' => false, 'error' => 'Image not found and pull failed: ' . ($pull_result['error'] ?? 'unknown')];
            }
        }

        if ($result['success']) {
            $container_id = $result['data']['Id'];

            // Start the container
            $start_result = dockerApiRequest('POST', "/containers/{$container_id}/start");

            if ($start_result['success']) {
                return ['success' => true, 'container_id' => $container_id];
            } else {
                return ['success' => false, 'error' => 'Container created but failed to start: ' . $start_result['error']];
            }
        }

        return $result;
    }
    
    /**
     * Create a user-defined Docker network if it does not exist.
     * Returns ['success' => bool, 'existed' => bool, 'error' => ?string].
     */
    public function create_network($name, $driver = 'bridge') {
        $inspect = dockerApiRequest('GET', '/networks/' . urlencode($name));
        if ($inspect['success']) {
            return ['success' => true, 'existed' => true];
        }
        // Internal: true means no default gateway — containers on this network cannot
        // reach the internet. Defense-in-depth: the Valkey container should only be
        // reachable from the customer's own app containers on this same private network.
        $result = dockerApiRequest('POST', '/networks/create', [
            'Name' => $name,
            'Driver' => $driver,
            'CheckDuplicate' => true,
            'Internal' => true,
            'Attachable' => true
        ]);
        return [
            'success' => $result['success'],
            'existed' => false,
            'error' => $result['error'] ?? null
        ];
    }

    /** Remove a Docker network. Idempotent. */
    public function remove_network($name) {
        $result = dockerApiRequest('DELETE', '/networks/' . urlencode($name));
        if (!$result['success'] && isset($result['error']) && stripos($result['error'], 'not found') !== false) {
            return ['success' => true];
        }
        return $result;
    }

    /**
     * Connect a container to a network, optionally with DNS aliases.
     * Idempotent — returns success if already connected.
     */
    public function connect_network($network, $container, $aliases = []) {
        $payload = ['Container' => $container];
        if (!empty($aliases)) {
            $payload['EndpointConfig'] = ['Aliases' => $aliases];
        }
        $result = dockerApiRequest('POST', '/networks/' . urlencode($network) . '/connect', $payload);
        if (!$result['success'] && isset($result['error']) && stripos($result['error'], 'already exists') !== false) {
            return ['success' => true];
        }
        return $result;
    }

    /** Disconnect a container from a network. Idempotent. */
    public function disconnect_network($network, $container, $force = true) {
        $payload = ['Container' => $container, 'Force' => $force];
        $result = dockerApiRequest('POST', '/networks/' . urlencode($network) . '/disconnect', $payload);
        if (!$result['success'] && isset($result['error']) &&
            (stripos($result['error'], 'not connected') !== false ||
             stripos($result['error'], 'not found') !== false)) {
            return ['success' => true];
        }
        return $result;
    }

    /**
     * Run a command inside a running container via the Docker exec API.
     * Returns ['success' => bool, 'exit_code' => int, 'output' => string, 'error' => ?string].
     */
    public function run_in_container($container, array $cmd) {
        $create = dockerApiRequest('POST', '/containers/' . urlencode($container) . '/exec', [
            'AttachStdout' => true,
            'AttachStderr' => true,
            'Cmd' => $cmd,
            'Tty' => false
        ]);
        if (!$create['success']) {
            return ['success' => false, 'exit_code' => -1, 'output' => '', 'error' => $create['error'] ?? 'create failed'];
        }
        $id = $create['data']['Id'] ?? null;
        if (!$id) {
            return ['success' => false, 'exit_code' => -1, 'output' => '', 'error' => 'no exec id'];
        }
        $start = dockerApiRequest('POST', '/exec/' . urlencode($id) . '/start', ['Detach' => false, 'Tty' => false], true);
        $output = is_string($start['data'] ?? null) ? $start['data'] : '';
        $inspect = dockerApiRequest('GET', '/exec/' . urlencode($id) . '/json');
        $rc = $inspect['success'] ? ($inspect['data']['ExitCode'] ?? -1) : -1;
        return [
            'success' => ($rc === 0),
            'exit_code' => $rc,
            'output' => $output,
            'error' => $rc === 0 ? null : trim($output)
        ];
    }

    /**
     * Trigger auto-recreation of containers for sites with auto_recreate_on_update enabled
     * This runs asynchronously in the background
     *
     * @param string $image_name The Docker image that was just pulled
     * @return void
     */
    private function triggerAutoRecreation($image_name) {
        // Run the recreation process in the background
        $command = sprintf(
            'nohup /usr/bin/php /root/whp/scripts/auto-recreate-containers.php %s >> /var/log/whp-auto-recreate.log 2>&1 &',
            escapeshellarg($image_name)
        );
        
        exec($command);
        
        error_log("Triggered auto-recreation for containers using image: $image_name");
    }
}

/**
 * Make a request to the Docker API
 *
 * @param string $method HTTP method (GET, POST, DELETE, etc.)
 * @param string $endpoint API endpoint (e.g. /containers/json)
 * @param array $data Data to be sent as JSON (for POST/PUT requests)
 * @return array Associative array containing 'success' flag and 'data' or 'error'
 */
function dockerApiRequest($method, $endpoint, $data = null, $raw = false) {
    $opts = [
        'http' => [
            'method' => $method,
            'header' => "Content-Type: application/json\r\n",
            'ignore_errors' => true
        ]
    ];
    
    if ($data !== null) {
        $opts['http']['content'] = json_encode($data);
    }
    
    $context = stream_context_create($opts);
    $url = "http://localhost/v1.44{$endpoint}";
    
    // Use cURL to connect to the Docker socket
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, DOCKER_SOCKET);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    if ($data !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    }
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($response === false) {
        return ['success' => false, 'error' => 'Failed to connect to Docker API'];
    }

    if ($raw) {
        return ['success' => $httpCode < 400, 'data' => $response];
    }

    $json = json_decode($response, true);

    if ($httpCode >= 200 && $httpCode < 300) {
        return ['success' => true, 'data' => $json ?: $response];
    } else {
        return ['success' => false, 'error' => isset($json['message']) ? $json['message'] : "Error: HTTP {$httpCode}"];
    }
}

// Canonical alias so both docker_api and DockerAPI refer to the same class.
class_alias('docker_api', 'DockerAPI');

/**
 * Create a new container for a domain
 *
 * @param int $domain_id Domain ID from database
 * @param string $domain_name Domain name (e.g. example.com)
 * @param int $container_type_id Container type ID from database
 * @param float $cpu_allowance CPU cores allocated
 * @param int $memory_allowance Memory allocated in MB
 * @return array Result with success/error information
 */
function createContainer($domain_id, $domain_name, $container_type_id, $cpu_allowance, $memory_allowance) {
    global $db;
    
    try {
        // Get container type information
        $stmt = $db->prepare("SELECT base_image FROM container_types WHERE id = ?");
        $stmt->execute([$container_type_id]);
        $container_type = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$container_type) {
            return ['success' => false, 'error' => 'Container type not found'];
        }
        
        $container_name = createContainerName($domain_name);
        $image = $container_type['base_image'];
        
        // Create container config
        $container_config = [
            'Image' => $image,
            'Hostname' => $domain_name,
            'Env' => [
                'VIRTUAL_HOST=' . $domain_name,
                'LETSENCRYPT_HOST=' . $domain_name,
                'DOMAIN=' . $domain_name
            ],
            'Labels' => [
                'domain_id' => (string)$domain_id,
                'domain_name' => $domain_name
            ],
            'HostConfig' => [
                'RestartPolicy' => [
                    'Name' => 'unless-stopped'
                ],
                'CpuPeriod' => 100000,
                'CpuQuota' => (int)(100000 * $cpu_allowance),
                'Memory' => $memory_allowance * 1024 * 1024,  // Convert MB to bytes
                'MemorySwap' => $memory_allowance * 1024 * 1024,  // Set swap equal to memory (no swap)
                'NetworkMode' => 'web-network'  // Use your actual network name
            ]
        ];
        
        // Create the container with name as URL parameter
        $result = dockerApiRequest('POST', '/containers/create?name=' . urlencode($container_name), $container_config);
        
        if (!$result['success']) {
            return $result;
        }
        
        $container_id = $result['data']['Id'];
        
        // Start the container
        $start_result = dockerApiRequest('POST', "/containers/{$container_id}/start");
        
        if (!$start_result['success']) {
            // If we can't start it, still save it but mark as stopped
            $container_status = 'stopped';
        } else {
            $container_status = 'running';
        }
        
        // Get container details to find IP address
        $inspect_result = dockerApiRequest('GET', "/containers/{$container_id}/json");
        
        if ($inspect_result['success']) {
            $network_settings = $inspect_result['data']['NetworkSettings'];
            $networks = $network_settings['Networks'];
            
            // Get the first available IP address
            $ip_address = null;
            foreach ($networks as $network) {
                if (isset($network['IPAddress']) && !empty($network['IPAddress'])) {
                    $ip_address = $network['IPAddress'];
                    break;
                }
            }
            
            // Get exposed port if any
            $port = null;
            if (!empty($network_settings['Ports'])) {
                foreach ($network_settings['Ports'] as $container_port => $host_bindings) {
                    if (!empty($host_bindings) && isset($host_bindings[0]['HostPort'])) {
                        $port = $host_bindings[0]['HostPort'];
                        break;
                    }
                }
            }
            
            // Save container info to database
            $stmt = $db->prepare("
                INSERT INTO containers (domain_id, container_id, ip_address, port, status) 
                VALUES (?, ?, ?, ?, ?)
            ");
            $stmt->execute([$domain_id, $container_id, $ip_address, $port, $container_status]);
            
            return [
                'success' => true, 
                'container_id' => $container_id,
                'ip_address' => $ip_address,
                'port' => $port,
                'status' => $container_status
            ];
        } else {
            // Save container info without IP
            $stmt = $db->prepare("
                INSERT INTO containers (domain_id, container_id, status) 
                VALUES (?, ?, ?)
            ");
            $stmt->execute([$domain_id, $container_id, $container_status]);
            
            return [
                'success' => true, 
                'container_id' => $container_id,
                'status' => $container_status
            ];
        }
    } catch (Exception $e) {
        return ['success' => false, 'error' => $e->getMessage()];
    }
}

/**
 * Stop a running container
 *
 * @param string $container_id Docker container ID
 * @return array Result with success/error information
 */
function stopContainer($container_id) {
    global $db;
    
    // Stop the container (with 10 second timeout)
    $result = dockerApiRequest('POST', "/containers/{$container_id}/stop?t=10");
    
    if ($result['success']) {
        // Update container status in database
        $stmt = $db->prepare("UPDATE containers SET status = 'stopped' WHERE container_id = ?");
        $stmt->execute([$container_id]);
        
        return ['success' => true];
    }
    
    return $result;
}

/**
 * Start a stopped container
 *
 * @param string $container_id Docker container ID
 * @return array Result with success/error information
 */
function startContainer($container_id) {
    global $db;
    
    // Start the container
    $result = dockerApiRequest('POST', "/containers/{$container_id}/start");
    
    if ($result['success']) {
        // Update container status in database
        $stmt = $db->prepare("UPDATE containers SET status = 'running' WHERE container_id = ?");
        $stmt->execute([$container_id]);
        
        // Get container details to find/update IP address
        $inspect_result = dockerApiRequest('GET', "/containers/{$container_id}/json");
        
        if ($inspect_result['success']) {
            $network_settings = $inspect_result['data']['NetworkSettings'];
            $networks = $network_settings['Networks'];
            
            // Get the first available IP address
            $ip_address = null;
            foreach ($networks as $network) {
                if (isset($network['IPAddress']) && !empty($network['IPAddress'])) {
                    $ip_address = $network['IPAddress'];
                    break;
                }
            }
            
            if ($ip_address) {
                $stmt = $db->prepare("UPDATE containers SET ip_address = ? WHERE container_id = ?");
                $stmt->execute([$ip_address, $container_id]);
            }
        }
        
        return ['success' => true];
    }
    
    return $result;
}

/**
 * Remove a container
 *
 * @param string $container_id Docker container ID
 * @return array Result with success/error information
 */
function removeContainer($container_id) {
    global $db;
    
    // Force remove the container (force=true, remove volumes=true)
    $result = dockerApiRequest('DELETE', "/containers/{$container_id}?force=true&v=true");
    
    if ($result['success']) {
        // Remove container from database
        $stmt = $db->prepare("DELETE FROM containers WHERE container_id = ?");
        $stmt->execute([$container_id]);
        
        return ['success' => true];
    }
    
    return $result;
}

/**
 * Update container resource limits
 *
 * @param string $container_id Docker container ID
 * @param float $cpu_allowance CPU cores allocated
 * @param int $memory_allowance Memory allocated in MB
 * @return array Result with success/error information
 */
function updateContainerResources($container_id, $cpu_allowance, $memory_allowance) {
    // Update container resources
    $data = [
        'CpuPeriod' => 100000,
        'CpuQuota' => (int)(100000 * $cpu_allowance),
        'Memory' => $memory_allowance * 1024 * 1024,  // Convert MB to bytes
        'MemorySwap' => $memory_allowance * 1024 * 1024  // Set swap equal to memory (no swap)
    ];
    
    $result = dockerApiRequest('POST', "/containers/{$container_id}/update", $data);
    
    return $result;
}

/**
 * Create a standardized container name from a domain name
 *
 * @param string $domain_name Domain name
 * @return string Container name
 */
function createContainerName($domain_name) {
    // Remove non-alphanumeric characters and replace with dash
    $name = preg_replace('/[^a-zA-Z0-9]/', '-', $domain_name);
    
    // Ensure name starts with a letter
    if (!preg_match('/^[a-zA-Z]/', $name)) {
        $name = 'site-' . $name;
    }
    
    // Limit length to 64 characters (Docker limitation)
    return substr($name, 0, 64);
}