<?php

/**
 * Initializes core constants used across the ip2nginx project.
 *
 * This script is **only** intended to be included by other scripts.
 * It prevents direct execution and ensures all key configuration constants are defined.
 */

// If the script is included and the FILE_IS_INCLUDED flag is defined,
// proceed to define the required constants.
if (defined('FILE_IS_INCLUDED')) {

  // === Core data paths ===

  // Define DATA_DIR if not already defined (used to store data files)
  if (!defined('DATA_DIR')) {
    define('DATA_DIR', __DIR__ . '/data');  // Directory where data files are stored
  }

  // Define METAFILE if not already defined (path to meta.json file)
  if (!defined('METAFILE')) {
    define('METAFILE', DATA_DIR . '/meta.json');  // Central file for domain metadata
  }

  // === Nginx configuration settings ===

  // Define VHOST_BASE if not already defined
  if (!defined('VHOST_BASE')) {
    define('VHOST_BASE', '/var/www/vhosts/system');  // Base path to vhost configs (Plesk)
  }

  // Define NGINX_CONF_NAME if not already defined
  if (!defined('NGINX_CONF_NAME')) {
    define('NGINX_CONF_NAME', 'nginx.conf');  // Target file to modify inside each vhost
  }

  // === Additional configuration ===

  // Define blocklist file
  if (!defined('BLOCKLIST_FILE')) {
    define('BLOCKLIST_FILE', DATA_DIR . '/blocklist.json');  // File storing blocked IPs
  }

  // Define authentication failure tracking file
  if (!defined('FAILURESFILE')) {
    define('FAILURESFILE', DATA_DIR . '/failures.json');  // Tracks failed login attempts
  }

  // Define general log file
  if (!defined('LOGFILE')) {
    define('LOGFILE', DATA_DIR . '/log.json');  // Change log
  }

  // Define API token file
  if (!defined('TOKENFILE')) {
    define('TOKENFILE', DATA_DIR . '/token.json');  // Token registry
  }

  // Define default location block used if none is set
  if (!defined('DEF_LOCATION')) {
    define('DEF_LOCATION', '/');  // Default nginx location path
  }

  // Define default example block for meta.json
  if (!defined('DEF_EXAMPLE')) {
    define('DEF_EXAMPLE', ["example.com" => "SECRET_EXAMPLE"]);
  }

  // === Security settings ===

  // Block duration in seconds (48 hours by default)
  if (!defined('BLOCK_DURATION_SECONDS')) {
    define('BLOCK_DURATION_SECONDS', 48 * 3600);  // Duration of IP block after failures
  }

} else {
  // If the file was accessed directly, deny access
  error_deny();
}

/**
 * Sends a standard 403 Forbidden HTTP response with an HTML message
 * and terminates the script.
 */
function error_deny() : void {
  // Set HTTP response status code to 403
  http_response_code(403);

  // Explicitly send HTTP headers for 403 Forbidden and HTML content type
  header('HTTP/1.1 403 Forbidden');
  header('Content-Type: text/html; charset=utf-8');

  // Output a simple HTML page explaining the forbidden access
  echo "<HTML>\n";
  echo "<HEAD>\n";
  echo "<TITLE>403 Forbidden</TITLE>\n";
  echo "</HEAD>\n";
  echo "<BODY>\n";
  echo "<H1>Forbidden</H1>\n";
  echo "You do not have permission to access this document.\n";
  echo "<P>\n";
  echo "<HR>\n";
  echo "<ADDRESS>\n";
  echo "Web Server at " . $_SERVER['SERVER_NAME'] . "\n";
  echo "</ADDRESS>\n";
  echo "</BODY>\n";
  echo "</HTML>\n";

  // Exit the script with status code 403
  exit(403);
}

/**
 * Retrieves a request parameter from POST or GET with fallback to default.
 *
 * @param string $name The name of the parameter to retrieve.
 * @param mixed  $def  The default value if the parameter is not found or empty.
 * @return mixed       The parameter value or the default.
 */
function getQuery(string $name, mixed $def): mixed {
  // Check POST first, then GET, fallback to default
  $val = $_POST[$name] ?? $_GET[$name] ?? $def;

  // Return default if value is empty (including trimmed empty strings)
  return isEmpty($val) ? $def : $val;
}

/**
 * Retrieves multiple request parameters from POST or GET with fallback to default.
 * Key matching is case-insensitive.
 *
 * @param array $params Array of parameters in the format:
 *                      [
 *                        'KeyName' => defaultValue,
 *                        ...
 *                      ]
 * @return array        Associative array:
 *                      [
 *                        'KeyName' => [
 *                          'value'   => mixed,
 *                          'source'  => 'post' | 'get' | 'default'
 *                        ],
 *                        ...
 *                      ]
 */
function getQueryMulti(array $params): array {
  $result = [];

  // Lowercase copies of GET and POST for case-insensitive matching
  $post = array_change_key_case($_POST, CASE_LOWER);
  $get  = array_change_key_case($_GET,  CASE_LOWER);

  foreach ($params as $key => $default) {
    $keyLower = strtolower($key);
    $value    = $default;
    $source   = 'default';

    if (isset($post[$keyLower]) && !isEmpty($post[$keyLower])) {
      $value  = $post[$keyLower];
      $source = 'post';
    } elseif (isset($get[$keyLower]) && !isEmpty($get[$keyLower])) {
      $value  = $get[$keyLower];
      $source = 'get';
    }

    $result[$key] = [
      'value'  => $value,
      'source' => $source
    ];
  }

  return $result;
}

/**
 * Checks if a given value is considered "empty".
 * - Returns true for null, empty strings, empty arrays, 0, "0", etc. (like PHP's `empty()`)
 * - Additionally, for strings, also trims whitespace before checking emptiness
 *
 * @param mixed $val The value to evaluate
 * @return bool True if the value is empty or whitespace-only, false otherwise
 */
function isEmpty(mixed $val): bool {
    // Handle typical empty values (null, "", 0, [], false, etc.)
    if (empty($val)) {
        return true;
    }

    // If it's a string, trim and check again
    if (is_string($val)) {
        if (trim($val) === '') {
            return true;
        }
    }

    return false;
}

/**
 * Loads and parses a JSON file into an associative array.
 *
 * Checks if the file exists and is readable before attempting to decode it.
 * If the JSON is invalid, an error message is printed.
 * If decoding fails or the result is not an array, an empty array is returned.
 *
 * @param string $filename Path to the JSON file
 * @return array Parsed associative array or empty array on error
 */
function loadJson(string $filename): array {
  $result = [];

  // Attempt to read and decode the file if it exists and is readable
  if (file_exists($filename) && is_readable($filename)) {
    $result = json_decode(file_get_contents($filename), true);

    // Log an error if JSON is invalid
    if (json_last_error() !== JSON_ERROR_NONE) {
      echo "Invalid JSON in $filename: " . json_last_error_msg() . "\n";
    }
  }

  // Ensure the result is an array
  if (!is_array($result)) {
    $result = [];
  }

  return $result;
}

/**
 * Saves a PHP array to a JSON file in pretty format.
 *
 * - Ensures the target file is writable if it already exists.
 * - Automatically replaces null or empty input with an empty array.
 * - Returns true on success, false on any failure.
 *
 * @param string     $filename The path to the JSON file to write
 * @param array|null $val      The data to save (null or empty → [])
 * @return bool                True if saved successfully, false otherwise
 */
function saveJson(string $filename, ?array $val): bool {
  // Default to empty array if null or empty
  if (isEmpty($val)) {
    $val = [];
  }

  // Encode to JSON
  $encoded = json_encode($val, JSON_PRETTY_PRINT);
  if ($encoded === false) {
    echo "Failed to encode $filename: " . json_last_error_msg() . "\n";
    return false;
  }

  // If file exists, check for write permissions
  if (file_exists($filename)) {
    if (!is_writable($filename)) {
      echo "No access to write in $filename\n";
      return false;
    }
  }
-+
  // Try writing up to 10 times with 250ms delay
  $success  = false;
  $attempt  = 0;
  for ($attempt = 1; $attempt <= 10; $attempt++) {
    if (@file_put_contents($filename, $encoded) !== false) {
      $success = true;
      break;
    }
    usleep(250 * 1000); // 250 ms delay
  }

  if (!$success) {
    echo "Failed to write to $filename after $attempt attempts\n";
  }

  return $success;
}
