<?php
/**
 * Enhanced PowerDNS Integration for WHP
 * 
 * This class provides comprehensive integration between PowerDNS and the WHP system,
 * including automatic zone creation, user management, and DNS record management.
 * 
 * @author Web Hosting Panel
 */

require_once('pdns-api.php');

class PowerDNSIntegration {
    private $pdnsApi;
    private $config;
    private $db;
    
    /**
     * Constructor
     * 
     * @param PDO $db Database connection
     */
    public function __construct($db) {
        $this->db = $db;
        $this->loadConfig();
        $this->initializeApi();
    }
    
    /**
     * Load PowerDNS configuration
     */
    private function loadConfig() {
        $config_file = '/docker/powerdns/config/whp-integration.conf';
        $passwords_file = '/docker/powerdns/config/passwords.conf';
        
        if (!file_exists($config_file)) {
            throw new Exception('PowerDNS configuration file not found: ' . $config_file);
        }
        
        if (!file_exists($passwords_file)) {
            throw new Exception('PowerDNS passwords file not found: ' . $passwords_file);
        }
        
        $this->config = [];
        
        // Load main configuration
        $lines = file($config_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            if (strpos($line, '#') === 0) continue; // Skip comments
            
            $parts = explode('=', $line, 2);
            if (count($parts) === 2) {
                $this->config[trim($parts[0])] = trim($parts[1]);
            }
        }
        
        // Load passwords from secure file
        $password_lines = file($passwords_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($password_lines as $line) {
            if (strpos($line, '#') === 0) continue; // Skip comments
            
            $parts = explode('=', $line, 2);
            if (count($parts) === 2) {
                $this->config[trim($parts[0])] = trim($parts[1]);
            }
        }
    }
    
    /**
     * Initialize PowerDNS API connection
     */
    private function initializeApi() {
        if (!isset($this->config['PDNS_API_URL']) || !isset($this->config['PDNS_API_KEY'])) {
            throw new Exception('PowerDNS API configuration missing');
        }
        
        $this->pdnsApi = new PowerDNSAPI(
            $this->config['PDNS_API_URL'],
            $this->config['PDNS_API_KEY'],
            $this->config['PDNS_SERVER_ID'] ?? 'localhost'
        );
    }
    
    /**
     * Create DNS zone for a domain when it's added to WHP
     * 
     * @param string $domain_name Domain name
     * @param string $username Owner username
     * @return array Result with success/error information
     */
    public function createZoneForDomain($domain_name, $username) {
        try {
            // Ensure domain has trailing dot
            $zone_name = $this->ensureTrailingDot($domain_name);
            
            // Check if zone already exists
            $existing_zone = $this->pdnsApi->getZone($zone_name);
            if ($existing_zone !== false) {
                return [
                    'success' => false,
                    'error' => 'DNS zone already exists for domain: ' . $domain_name
                ];
            }
            
            // Create nameserver entries
            $nameservers = [
                'ns1.' . $zone_name,
                'ns2.' . $zone_name
            ];
            
            // Create the zone
            $result = $this->pdnsApi->createZone($zone_name, $nameservers, 'Native');
            
            if ($result === false) {
                return [
                    'success' => false,
                    'error' => 'Failed to create DNS zone: ' . $this->pdnsApi->getLastError()
                ];
            }
            
            // Add default DNS records
            $this->addDefaultRecords($domain_name, $username);
            
            // Log the zone creation
            $this->logZoneOperation($domain_name, $username, 'create', 'success');
            
            return [
                'success' => true,
                'zone_name' => $zone_name,
                'message' => 'DNS zone created successfully for domain: ' . $domain_name
            ];
            
        } catch (Exception $e) {
            $this->logZoneOperation($domain_name, $username, 'create', 'error', $e->getMessage());
            return [
                'success' => false,
                'error' => 'Exception creating DNS zone: ' . $e->getMessage()
            ];
        }
    }
    
    /**
     * Delete DNS zone when domain is removed from WHP
     * 
     * @param string $domain_name Domain name
     * @param string $username Owner username
     * @return array Result with success/error information
     */
    public function deleteZoneForDomain($domain_name, $username) {
        try {
            $zone_name = $this->ensureTrailingDot($domain_name);
            
            $result = $this->pdnsApi->deleteZone($zone_name);
            
            if (!$result) {
                $this->logZoneOperation($domain_name, $username, 'delete', 'error', $this->pdnsApi->getLastError());
                return [
                    'success' => false,
                    'error' => 'Failed to delete DNS zone: ' . $this->pdnsApi->getLastError()
                ];
            }
            
            $this->logZoneOperation($domain_name, $username, 'delete', 'success');
            
            return [
                'success' => true,
                'message' => 'DNS zone deleted successfully for domain: ' . $domain_name
            ];
            
        } catch (Exception $e) {
            $this->logZoneOperation($domain_name, $username, 'delete', 'error', $e->getMessage());
            return [
                'success' => false,
                'error' => 'Exception deleting DNS zone: ' . $e->getMessage()
            ];
        }
    }
    
    /**
     * Add default DNS records for a new domain
     * 
     * @param string $domain_name Domain name
     * @param string $username Owner username
     */
    private function addDefaultRecords($domain_name, $username) {
        // Get server IP address
        $server_ip = $this->getServerIP();
        
        // Get container IP if available
        $container_ip = $this->getContainerIP($domain_name);
        $target_ip = $container_ip ?: $server_ip;
        
        // Get server settings
        $server_settings = $this->getServerSettings();
        
        // Get nameservers from whp_nameservers configuration
        if (isset($server_settings['whp_nameservers'])) {
            $nameservers = [
                $server_settings['whp_nameservers']['ns1'] ?? 'ns1.' . $domain_name,
                $server_settings['whp_nameservers']['ns2'] ?? 'ns2.' . $domain_name
            ];
        } else {
            $nameservers = $server_settings['nameservers'] ?? ['ns1.' . $domain_name, 'ns2.' . $domain_name];
        }
        $mail_server = $server_settings['mail_server'] ?? 'mail.' . $domain_name;
        $default_ttl = isset($server_settings['default_ttl']) ? max(300, intval($server_settings['default_ttl'])) : 3600;
        
        // Add SOA record (must be first for proper zone initialization)
        $primary_ns = $nameservers[0] ?? 'ns1.' . $domain_name;
        $soa_content = $primary_ns . '. admin.' . $domain_name . '. ' . time() . ' 10800 3600 604800 3600';
        $this->addRecord($domain_name, '@', 'SOA', $soa_content, $default_ttl);
        
        // Add A record for the domain itself (apex)
        $this->addRecord($domain_name, '@', 'A', $target_ip, $default_ttl);
        
        // Add CNAME record for www pointing to apex
        $this->addRecord($domain_name, 'www', 'CNAME', $domain_name . '.', $default_ttl);
        
        // Add NS records using configured nameservers (all at once)
        $ns_contents = [];
        foreach ($nameservers as $ns) {
            $ns_contents[] = $ns . '.';
        }
        $this->addMultipleRecords($domain_name, '@', 'NS', $default_ttl, $ns_contents);
        
        $nameserver_ips = $server_settings['nameserver_ips'] ?? ['', ''];
        // Add A records for nameservers if they're subdomains
        foreach ($nameservers as $idx => $ns) {
            if (strpos($ns, $domain_name) !== false) {
                $ns_name = str_replace('.' . $domain_name, '', $ns);
                $ip = $nameserver_ips[$idx] ?? $server_ip;
                if (!empty($ip)) {
                    $this->addRecord($domain_name, $ns_name, 'A', $ip, $default_ttl);
                }
            }
        }
        
        // Add MX record pointing to mail server
        $this->addRecord($domain_name, '@', 'MX', $mail_server . '.', $default_ttl, 10);
        
        // Add A record for mail server if it's a subdomain
        if (strpos($mail_server, $domain_name) !== false) {
            $mail_name = str_replace('.' . $domain_name, '', $mail_server);
            $this->addRecord($domain_name, $mail_name, 'A', $server_ip, $default_ttl);
        }
        
        // Add TXT record for SPF
        $this->addRecord($domain_name, '@', 'TXT', '"v=spf1 a mx ip4:' . $server_ip . ' ~all"', $default_ttl);
    }

    /**
     * Update NS records for all domains when nameserver settings change
     */
    public function updateAllDomainNSRecords() {
        // Get current nameservers from settings
        $server_settings = $this->getServerSettings();
        $nameservers = $server_settings['nameservers'] ?? ['ns1.example.com', 'ns2.example.com'];
        
        // Get all domains
        $stmt = $this->db->query('SELECT domain_name FROM whp.domains');
        $domains = $stmt->fetchAll(PDO::FETCH_COLUMN);
        
        foreach ($domains as $domain_name) {
            $zone_name = $this->ensureTrailingDot($domain_name);
            // Remove all existing NS records for this domain
            $existing_records = $this->getRecords($domain_name);
            foreach ($existing_records as $record) {
                if ($record['type'] === 'NS') {
                    $this->deleteRecord($domain_name, '@', 'NS');
                }
            }
            // Add new NS records from settings (all at once)
            $ns_contents = [];
            foreach ($nameservers as $ns) {
                $ns_contents[] = $ns . '.';
            }
            $this->addMultipleRecords($domain_name, '@', 'NS', 3600, $ns_contents);
        }
    }
    
    /**
     * Add a DNS record
     * 
     * @param string $domain_name Domain name
     * @param string $name Record name
     * @param string $type Record type
     * @param string $content Record content
     * @param int $ttl TTL in seconds
     * @param int|null $priority Priority (for MX records)
     * @return bool Success status
     */
    public function addRecord($domain_name, $name, $type, $content, $ttl = 3600, $priority = null) {
        $zone_name = $this->ensureTrailingDot($domain_name);
        
        // Enforce minimum TTL of 300 seconds
        $ttl = max(300, intval($ttl));
        
        // Format the record name correctly
        if (substr($name, -strlen($domain_name) - 1) === '.' . $domain_name) {
            $record_name = $this->ensureTrailingDot($name);
        } else if ($name === $domain_name || $name === '@') {
            $record_name = $zone_name;
        } else {
            $record_name = $name . '.' . $zone_name;
        }
        
        // For MX records, include priority in content
        $record_content = $content;
        if (strtoupper($type) === 'MX' && $priority !== null) {
            $record_content = $priority . ' ' . $content;
        }
        
        // Ensure proper formatting for certain record types
        if (in_array(strtoupper($type), ['CNAME', 'MX', 'NS', 'SRV'])) {
            $record_content = $this->ensureTrailingDot($record_content);
        }
        
        $result = $this->pdnsApi->addRecord($zone_name, $record_name, strtoupper($type), $ttl, [$record_content]);
        
        if ($result) {
            $this->logRecordOperation($domain_name, $name, $type, 'add', 'success');
        } else {
            $this->logRecordOperation($domain_name, $name, $type, 'add', 'error', $this->pdnsApi->getLastError());
        }
        
        return $result;
    }

    /**
     * Add multiple DNS records in a single API call
     * 
     * @param string $domain_name Domain name
     * @param string $name Record name
     * @param string $type Record type
     * @param int $ttl TTL in seconds
     * @param array $contents Array of record contents
     * @return bool Success status
     */
    public function addMultipleRecords($domain_name, $name, $type, $ttl, $contents) {
        $zone_name = $this->ensureTrailingDot($domain_name);
        
        // Enforce minimum TTL of 300 seconds
        $ttl = max(300, intval($ttl));
        
        // Format the record name correctly
        if (substr($name, -strlen($domain_name) - 1) === '.' . $domain_name) {
            $record_name = $this->ensureTrailingDot($name);
        } else if ($name === $domain_name || $name === '@') {
            $record_name = $zone_name;
        } else {
            $record_name = $name . '.' . $zone_name;
        }

        // Ensure proper formatting for certain record types
        if (in_array(strtoupper($type), ['CNAME', 'MX', 'NS', 'SRV'])) {
            foreach ($contents as &$content) {
                $content = $this->ensureTrailingDot($content);
            }
        }

        $result = $this->pdnsApi->addMultipleRecords($zone_name, $record_name, strtoupper($type), $ttl, $contents);
        
        if ($result) {
            $this->logRecordOperation($domain_name, $name, $type, 'add_multiple', 'success');
        } else {
            $this->logRecordOperation($domain_name, $name, $type, 'add_multiple', 'error', $this->pdnsApi->getLastError());
        }
        
        return $result;
    }
    
    /**
     * Delete a DNS record
     * 
     * @param string $domain_name Domain name
     * @param string $name Record name
     * @param string $type Record type
     * @return bool Success status
     */
    public function deleteRecord($domain_name, $name, $type) {
        $zone_name = $this->ensureTrailingDot($domain_name);
        
        // Format the record name correctly
        if (substr($name, -strlen($domain_name) - 1) === '.' . $domain_name) {
            $record_name = $this->ensureTrailingDot($name);
        } else if ($name === $domain_name || $name === '@') {
            $record_name = $zone_name;
        } else {
            $record_name = $name . '.' . $zone_name;
        }
        
        $result = $this->pdnsApi->deleteRecord($zone_name, $record_name, strtoupper($type));
        
        if ($result) {
            $this->logRecordOperation($domain_name, $name, $type, 'delete', 'success');
        } else {
            $this->logRecordOperation($domain_name, $name, $type, 'delete', 'error', $this->pdnsApi->getLastError());
        }
        
        return $result;
    }
    
    /**
     * Get all DNS records for a domain
     * 
     * @param string $domain_name Domain name
     * @return array Records array or false on error
     */
    public function getRecords($domain_name) {
        $zone_name = $this->ensureTrailingDot($domain_name);
        
        $records = $this->pdnsApi->getRecords($zone_name);
        
        if ($records === false) {
            return false;
        }
        
        // Process records for easier consumption
        $processed_records = [];
        foreach ($records as $record) {
            // Skip SOA records for simpler display
            if ($record['type'] == 'SOA') {
                continue;
            }
            
            foreach ($record['records'] as $content) {
                $processed_records[] = [
                    'name' => rtrim($record['name'], '.'),
                    'type' => $record['type'],
                    'ttl' => $record['ttl'],
                    'content' => $content['content'],
                    'disabled' => $content['disabled']
                ];
            }
        }
        
        return $processed_records;
    }
    
    /**
     * Get server settings from configuration
     * 
     * @return array Server settings
     */
    private function getServerSettings() {
        $settings_file = '/docker/whp/settings.json';
        
        if (file_exists($settings_file)) {
            $settings_content = file_get_contents($settings_file);
            if ($settings_content) {
                $settings = json_decode($settings_content, true);
                if ($settings) {
                    return $settings;
                }
            }
        }
        
        // Return default settings if file doesn't exist or is invalid
        return [
            'nameservers' => ['ns1.example.com', 'ns2.example.com'],
            'mail_server' => 'mail.example.com'
        ];
    }
    
    /**
     * Get server IP address
     * 
     * @return string Server IP address
     */
    private function getServerIP() {
        // First check server settings for configured IP
        $server_settings = $this->getServerSettings();
        if (!empty($server_settings['server_ip'])) {
            return $server_settings['server_ip'];
        }
        
        // Try multiple methods to get the server IP
        $server_ip = null;
        
        // Method 1: Try primary network interface
        $output = shell_exec("ip route get 8.8.8.8 2>/dev/null | awk '{print $7}' | head -1");
        if ($output && trim($output) !== '') {
            $server_ip = trim($output);
        }
        
        // Method 2: Try getting primary interface IP
        if (!$server_ip) {
            $output = shell_exec("hostname -I 2>/dev/null | awk '{print $1}'");
            if ($output && trim($output) !== '') {
                $server_ip = trim($output);
            }
        }
        
        // Method 3: Try SERVER_ADDR environment variable
        if (!$server_ip) {
            $server_ip = $_SERVER['SERVER_ADDR'] ?? null;
        }
        
        // Method 4: Try getting from primary network interface directly
        if (!$server_ip || $server_ip === '127.0.0.1' || $server_ip === '::1') {
            $output = shell_exec("ip -4 addr show scope global | grep inet | head -1 | awk '{print $2}' | cut -d/ -f1 2>/dev/null");
            if ($output && trim($output) !== '') {
                $server_ip = trim($output);
            }
        }
        
        // Method 5: For development/test environments, check common internal ranges
        if (!$server_ip || $server_ip === '127.0.0.1') {
            // Check for common internal IP patterns
            $interfaces = shell_exec("ip addr show 2>/dev/null | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d/ -f1");
            if ($interfaces) {
                $ips = explode("\n", trim($interfaces));
                foreach ($ips as $ip) {
                    $ip = trim($ip);
                    // Prefer 192.168.x.x, 10.x.x.x, or 172.16-31.x.x ranges for internal networks
                    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && $ip !== '127.0.0.1') {
                        $server_ip = $ip;
                        break;
                    }
                }
            }
        }
        
        // Final fallback
        if (!$server_ip) {
            $server_ip = '127.0.0.1';
            error_log("PowerDNS: Warning - Could not determine server IP, using localhost");
        }
        
        return $server_ip;
    }
    
    /**
     * Get container IP for a domain
     * 
     * @param string $domain_name Domain name
     * @return string|null Container IP or null if not found
     */
    private function getContainerIP($domain_name) {
        try {
            $stmt = $this->db->prepare("
                SELECT c.ip_address 
                FROM containers c 
                JOIN domains d ON c.domain_id = d.id 
                WHERE d.domain_name = ?
            ");
            $stmt->execute([$domain_name]);
            $result = $stmt->fetch(PDO::FETCH_ASSOC);
            
            return $result ? $result['ip_address'] : null;
        } catch (Exception $e) {
            return null;
        }
    }
    
    /**
     * Ensure domain name has trailing dot
     * 
     * @param string $domain Domain name
     * @return string Domain name with trailing dot
     */
    private function ensureTrailingDot($domain) {
        if (substr($domain, -1) !== '.') {
            return $domain . '.';
        }
        return $domain;
    }
    
    /**
     * Log zone operations
     * 
     * @param string $domain_name Domain name
     * @param string $username Username
     * @param string $operation Operation type
     * @param string $status Success/error
     * @param string $error_message Error message if applicable
     */
    private function logZoneOperation($domain_name, $username, $operation, $status, $error_message = null) {
        $log_file = '/docker/powerdns/logs/zone-operations.log';
        $timestamp = date('Y-m-d H:i:s');
        $message = "[{$timestamp}] ZONE_{$operation}: {$domain_name} by {$username} - {$status}";
        
        if ($error_message) {
            $message .= " - Error: {$error_message}";
        }
        
        file_put_contents($log_file, $message . PHP_EOL, FILE_APPEND | LOCK_EX);
    }
    
    /**
     * Log record operations
     * 
     * @param string $domain_name Domain name
     * @param string $record_name Record name
     * @param string $record_type Record type
     * @param string $operation Operation type
     * @param string $status Success/error
     * @param string $error_message Error message if applicable
     */
    private function logRecordOperation($domain_name, $record_name, $record_type, $operation, $status, $error_message = null) {
        $log_file = '/docker/powerdns/logs/record-operations.log';
        $timestamp = date('Y-m-d H:i:s');
        $message = "[{$timestamp}] RECORD_{$operation}: {$domain_name} {$record_name} {$record_type} - {$status}";
        
        if ($error_message) {
            $message .= " - Error: {$error_message}";
        }
        
        file_put_contents($log_file, $message . PHP_EOL, FILE_APPEND | LOCK_EX);
    }
    
    /**
     * Get PowerDNS statistics
     * 
     * @return array Statistics array
     */
    public function getStatistics() {
        try {
            $zones = $this->pdnsApi->getZones();
            $total_zones = is_array($zones) ? count($zones) : 0;
            
            $total_records = 0;
            if (is_array($zones)) {
                foreach ($zones as $zone) {
                    $records = $this->pdnsApi->getRecords($zone['name']);
                    if (is_array($records)) {
                        $total_records += count($records);
                    }
                }
            }
            
            return [
                'total_zones' => $total_zones,
                'total_records' => $total_records,
                'api_status' => 'connected'
            ];
        } catch (Exception $e) {
            return [
                'total_zones' => 0,
                'total_records' => 0,
                'api_status' => 'error',
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * Check if PowerDNS is healthy
     * 
     * @return bool Health status
     */
    public function isHealthy() {
        try {
            $result = $this->pdnsApi->getZones();
            return $result !== false;
        } catch (Exception $e) {
            return false;
        }
    }
    
    /**
     * Get configuration
     * 
     * @return array Configuration array
     */
    public function getConfig() {
        return $this->config;
    }

    /**
     * Get all FQDNs (apex and subdomains) for a user's domains, filtered by record type (A/CNAME)
     * @param string $username
     * @param array $include_types (default: ['A','CNAME'])
     * @return array [['fqdn'=>..., 'type'=>..., 'domain'=>...], ...]
     */
    public function getUserDomainFQDNs($username, $include_types = ['A','CNAME']) {
        $result = [];
        $is_root = ($username === 'root');
        // Get all domains for user
        if ($is_root) {
            $stmt = $this->db->query("SELECT domain_name, username FROM whp.domains WHERE active = 1");
            $domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
        } else {
            $stmt = $this->db->prepare("SELECT domain_name, username FROM whp.domains WHERE username = ? AND active = 1");
            $stmt->execute([$username]);
            $domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
        }
        foreach ($domains as $domain_row) {
            $domain = $domain_row['domain_name'];
            $records = $this->getRecords($domain);
            if (!is_array($records)) continue;
            foreach ($records as $rec) {
                if (in_array(strtoupper($rec['type']), $include_types)) {
                    // FQDN: if name == domain, it's apex; else it's subdomain
                    $fqdn = $rec['name'];
                    if (stripos($fqdn, $domain) === false) {
                        $fqdn = $fqdn . '.' . $domain;
                    }
                    $result[] = [
                        'fqdn' => $fqdn,
                        'type' => strtoupper($rec['type']),
                        'domain' => $domain,
                        'record_name' => $rec['name'],
                        'content' => $rec['content'],
                        'is_wildcard' => (strpos($fqdn, '*.') === 0)
                    ];
                }
            }
        }
        // Sort by domain then fqdn
        usort($result, function($a, $b) {
            return [$a['domain'], $a['fqdn']] <=> [$b['domain'], $b['fqdn']];
        });
        return $result;
    }

    /**
     * Add a wildcard A record for a domain (*.domain.tld)
     *
     * @param string $domain_name Base domain name (e.g., domain.tld)
     * @param string|null $target_ip Target IP address (defaults to server IP)
     * @return bool Success status
     */
    public function addWildcardRecord($domain_name, $target_ip = null) {
        if ($target_ip === null) {
            $target_ip = $this->getServerIP();
        }

        $server_settings = $this->getServerSettings();
        $default_ttl = isset($server_settings['default_ttl']) ? max(300, intval($server_settings['default_ttl'])) : 3600;

        return $this->addRecord($domain_name, '*', 'A', $target_ip, $default_ttl);
    }

    /**
     * Remove a wildcard A record for a domain (*.domain.tld)
     *
     * @param string $domain_name Base domain name (e.g., domain.tld)
     * @return bool Success status
     */
    public function removeWildcardRecord($domain_name) {
        return $this->deleteRecord($domain_name, '*', 'A');
    }

    /**
     * Add a DNS-01 ACME challenge TXT record (_acme-challenge.domain.tld)
     *
     * @param string $domain_name Base domain name (e.g., domain.tld)
     * @param string $challenge_value The challenge token value for the TXT record
     * @return bool Success status
     */
    public function addDnsChallengeRecord($domain_name, $challenge_value) {
        // Validate ACME token format (base64url characters only)
        if (!preg_match('/^[A-Za-z0-9_\-]+$/', $challenge_value)) {
            error_log("PowerDNS: Invalid ACME challenge token format");
            return false;
        }
        return $this->addRecord($domain_name, '_acme-challenge', 'TXT', '"' . $challenge_value . '"', 300);
    }

    /**
     * Remove a DNS-01 ACME challenge TXT record
     *
     * @param string $domain_name Base domain name (e.g., domain.tld)
     * @return bool Success status
     */
    public function removeDnsChallengeRecord($domain_name) {
        return $this->deleteRecord($domain_name, '_acme-challenge', 'TXT');
    }
} 