<?php
// Define a constant to indicate index.php is included, not accessed directly
define('FILE_IS_INCLUDED', true);

// Include the index.php file, which contains shared configuration and logic
include('index.php');

// Only allow this script to run from the command line interface
if (php_sapi_name() !== 'cli') {
  error_deny();  // Deny access if run from a browser or non-CLI environment
}

/**
 * Main logic handler
 */
main();

/**
 * Main routine that processes domain updates based on `meta.json`.
 *
 * - Loads domain configuration data.
 * - Applies updates for domains where `"changed": 1`.
 * - Resets the change flag after successful update.
 * - Reloads NGINX only if at least one domain was updated.
 * - Saves the updated metadata back to disk.
 *
 * Exits with error code 1 on failure to save updated data.
 */
function main(): void {
  // Load content of meta.json
  $domains = loadJson(METAFILE);

  // Ensure meta is valid and contains domain entries
  if (isEmpty($domains)) {
    echo "No valid domains in meta.json\n";
    exit(1);
  }

  // Loop through all domains and apply updates if 'changed' flag is set
  $changed = false;
  foreach ($domains as $name => $meta) {
    // Process and detect if actual change occurred
    if (processDomain($name, $meta)) {
      // Reset 'changed' flag after processing
      $domains[$name]['changed'] = 0;
      $changed  = true;
    }
  }

  // Restart nginx if any updates were made and save new state
  if ($changed) {
    shell_exec("sudo nginx -t && sudo systemctl reload nginx");
    echo "Reloaded nginx configuration.\n";

    // Save updated meta.json back to file
    if (!saveJson(METAFILE, $domains)) {
      exit(1);
    }
  }

  echo "All applicable domains processed.\n";
}

/**
 * Generic processor for a location block in NGINX config lines.
 * Applies a callback to each line inside the matched location block.
 *
 * @param array  $lines       NGINX config lines
 * @param string $path        Location path to match (e.g. "/api/", "\.php$")
 * @param string|null $modifier Location modifier (e.g. "~", "^~", "=", or null)
 * @param callable $callback  Callback: function(array &$lines, int $i): void
 * @return array              Modified lines
 */
function processLocationBlock(array $lines, string $path, ?string $modifier, callable $callback): array {
  $inBlock     = false;
  $braceLevel  = 0;
  $escapedPath = preg_quote($path, '/');

  if ($modifier !== null) {
    $pattern = "/^\\s*location\\s+{$escapedPath}\\s+$modifier\\s*\\{/"; // e.g. location /api/ ^~ {
  } else {
    $pattern = "/^\\s*location\\s+{$escapedPath}\\s*\\{/";              // e.g. location /api/ {
  }

  foreach ($lines as $i => $line) {
    if (!$inBlock && preg_match($pattern, $line)) {
      $inBlock    = true;
      $braceLevel = substr_count($line, '{') - substr_count($line, '}');
      $callback($lines, $i, 'start');
      continue;
    }

    if ($inBlock) {
      $braceLevel += substr_count($line, '{');
      $braceLevel -= substr_count($line, '}');

      $callback($lines, $i, 'inside');

      if ($braceLevel <= 0) {
        $callback($lines, $i, 'end');
        $inBlock = false;
      }
    }
  }

  return $lines;
}

/**
 * Removes a specific `location` block from an array of NGINX configuration lines.
 *
 * This function uses a helper (`processLocationBlock`) to identify a `location` block
 * by its path and optional modifier (e.g., `~`, `=`, `^~`), and removes the entire block
 * including its contents from the provided config lines.
 *
 * @param array &$lines  NGINX config lines.
 * @param string $path   The location path to match (e.g., "/api", "\.php$").
 * @param string|null $modifier Optional location modifier (e.g. `~`, `=`, `^~`). If null, matches unmodified location.
 * @return array         A new array of config lines with the matched block removed.
 */
function removeLocationBlock(array $lines, string $path, ?string $modifier = null): array {
  $result     = [];       // Final result array for cleaned config lines
  $skipBlock  = false;    // Flag indicating whether we are inside a block to remove
  $buffer     = [];       // Temporary buffer for skipped lines (optional, unused here)

  // Process the location block using a callback to mark lines for deletion
  $lines = processLocationBlock($lines, $path, $modifier, function(array &$lines, int $i, string $phase) use (&$skipBlock, &$buffer) {
    if ($phase === 'start') {
      $skipBlock = true;        // Begin removing lines at block start
    }
    if ($skipBlock) {
      $lines[$i] = null;        // Mark current line for removal
    }
    if ($phase === 'end') {
      $skipBlock = false;       // Stop skipping lines at block end
    }
  });

  // Remove all lines marked as null (deleted) and reindex
  foreach ($lines as $line) {
    if (!isEmpty($line)) {
      $result[] = $line;
    }
  }

  return $result;
}

/**
 * Updates one or more NGINX directives inside a specific location block.
 * Also optionally appends the directive if it's not found in the block.
 *
 * @param array        $lines         NGINX config lines as an array
 * @param string       $locationPath  The location path to match (e.g. "/api/")
 * @param string|null  $modifier      Modifier for location (e.g. "~", "^~") or null for plain
 * @param array        $replacements  Array of directive replacements in the format:
 *                                    [
 *                                      [
 *                                        'name' => 'directive_name',
 *                                        'val'  => 'replacement value',
 *                                        'append' => true|false (optional)
 *                                      ],
 *                                      ...
 *                                    ]
 * @return array                      Modified lines with replacements and optional appends
 */
function updateLocationBlock(array $lines, string $locationPath, ?string $modifier, array $replacements): array {
  $seenDirectives = [];  // Tracks which directives were matched and replaced

  return processLocationBlock($lines, $locationPath, $modifier, function(array &$lines, int $i, string $phase) use ($replacements, &$seenDirectives) {
    // This block runs for each line inside the matching location block

    if ($phase === 'inside') {
      foreach ($replacements as $rep) {
        // Validate required fields
        if (!isset($rep['name'], $rep['val'])) continue;

        $name = preg_quote($rep['name'], '/'); // Escape directive name for regex
        $val  = $rep['val'];

        // If line contains the directive, replace its value
        if (preg_match("/^\\s*{$name}\\s+.+;/", $lines[$i])) {
          $lines[$i] = preg_replace("/^\\s*{$name}\\s+.+;/", "{$rep['name']} {$val};", $lines[$i]);
          $seenDirectives[$rep['name']] = true; // Mark directive as seen
        }
      }
    }

    // At the end of the location block, insert any missing directives marked for appending
    if ($phase === 'end') {
      $inserts = [];

      foreach ($replacements as $rep) {
        $append = $rep['append'] ?? false;
        if (!isset($rep['name'], $rep['val']) || !$append) continue;

        // If directive wasn't seen inside the block, add it now
        if (empty($seenDirectives[$rep['name']])) {
          $inserts[] = "  {$rep['name']} {$rep['val']};";  // Indentation is preserved
        }
      }

      // Insert new lines right before the closing brace of the location block
      if (!empty($inserts)) {
        array_splice($lines, $i, 0, $inserts);
      }
    }
  });
}

/**
 * Processes a single domain configuration from meta.json.
 *
 * - Verifies all required fields are present and non-empty.
 * - Constructs the expected proxy_pass value from protocol, domain, and port.
 * - Replaces only the proxy_pass directive within the specified location block.
 * - If the config changes, it is saved and NGINX will be marked for reload.
 * - Even if no actual config differences are found, a restart can still be triggered
 *   by manually setting `"changed": 1` in meta.json.
 *
 * @param string $name The key in meta.json (usually target vhost name)
 * @param array  $meta The domain's metadata including IP, protocol, port, location, etc.
 * @return bool        True if a reload should be performed, false otherwise
 */
function processDomain(string $name, array $meta): bool {
  $requiredFields = ['domain', 'ip', 'protocol', 'port', 'location'];  // Required keys
  $missing        = [];
  $result         = false;

  // Collect any missing or empty required fields
  foreach ($requiredFields as $field) {
    if (isEmpty($meta[$field])) {
      $missing[] = $field;
    }
  }

  // Skip this domain if required fields are missing
  if (!isEmpty($missing)) {
    echo "Skipping '$name': missing or empty field(s): " . implode(', ', $missing) . "\n";
    return false;
  }

  // Allow forced reloads even without content change by setting "changed": 1 in meta.json
  $result = boolval($meta['changed'] ?? 1);

  // Full path to nginx config file for this domain
  $systemConfPath = VHOST_BASE . '/' . $name . '/conf/' . NGINX_CONF_NAME;

  // Check file existence and permissions
  if (!file_exists($systemConfPath)) {
    echo "System config not found for $name\n";
    return false;
  }
  if (!is_readable($systemConfPath) || !is_writable($systemConfPath)) {
    echo "Permission denied for $systemConfPath\n";
    return false;
  }

  // Read current nginx config
  $original = file_get_contents($systemConfPath);
  $lines    = explode("\n", $original); // Split config into lines

  // Build new proxy_pass target
  $proxyTarget = $meta['protocol'] . '://' . $meta['domain'] . ':' . $meta['port'];

  // Apply transformation inside matching location block
  $lines    = updateLocationBlock($lines, $meta['location'], null, [['name' => 'proxy_pass', 'val' => $proxyTarget]]);

  // Remove the PHP handler location block from nginx config.
  // This prevents local PHP processing and ensures the request is forwarded to the remote upstream server.
  $lines    = removeLocationBlock($lines, '~', '.*\.php.*');

  $modified = implode("\n", $lines);
  // Write only if content changed
  if ($original !== $modified) {
    $result = (file_put_contents($systemConfPath, $modified) !== false);
    echo "Domain: $name is updated\n";
  }

  return $result;
}
