<?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');

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

/**
 * Main entry point for processing update requests.
 *
 * This function:
 * - Validates environment and access permissions.
 * - Retrieves input parameters from POST/GET using getQueryMulti().
 * - Applies security logic: if any input came from GET, assumes a potential attack and resets IP/domain.
 * - Validates required fields and token.
 * - Triggers metadata update and logging if valid.
 */
function main(): void {
  // Ensure the data directory exists and is writable
  if (!ensureDirectoryExists(DATA_DIR)) {
    error_deny();
  }

  // Get the real client IP address
  $clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';

  // Deny access if the client IP is blocked
  if (isBlockedIP($clientIp)) {
    error_deny();
  }

  // Retrieve all expected input parameters using case-insensitive matching
  $query = getQueryMulti([
    'name'     => null,
    'token'    => null,
    'ip'       => $clientIp,
    'domain'   => $clientIp, // fallback to IP if domain not given
    'protocol' => 'https',
    'port'     => null,
  ]);

  // Check if any parameter was received via GET (possible tampering)
  $usedGET = false;
  foreach ($query as $key => $meta) {
    if ($meta['source'] === 'get') {
      $usedGET = true;
      break;
    }
  }

  // Extract values
  $name     = $query['name']['value'];
  $domain   = $query['domain']['value'];
  $ip       = $query['ip']['value'];
  $token    = $query['token']['value'];
  $port     = $query['port']['value'];
  $protocol = $query['protocol']['value'];

  // If GET was used, override domain and IP with server-detected IP for safety
  if ($usedGET) {
    $domain = $clientIp;
    $ip     = $clientIp;
  }

  // Load token list
  $tokens = loadTokens();

  // Basic validation
  if (isEmpty($name) || isEmpty($token) || !filter_var($ip, FILTER_VALIDATE_IP)) {
    error_deny();
  }

  // Token mismatch
  if (!array_key_exists($name, $tokens) || $tokens[$name] !== $token) {
    recordFailure($clientIp);
    error_deny();
  }

  // Validate protocol
  if (!in_array($protocol, ['http', 'https'])) {
    error_deny();
  }

  // Normalize port if empty or invalid
  if (isEmpty($port)) {
    $port = ($protocol === 'https') ? 443 : 80;
  } elseif (!preg_match('/^\d{2,5}$/', (string)$port)) {
    error_deny();
  }
  $port = intval($port);

  // All good: update metadata and log
  updateMeta($name, $domain, $ip, $port, $protocol);
}

/**
 * Loads token list from TOKENFILE or creates an example if file does not exist
 */
function loadTokens(): array {
    if (!file_exists(TOKENFILE)) {
      saveJson(TOKENFILE, DEF_EXAMPLE);
      echo    "Token file created at " . TOKENFILE . ". Please edit it and rerun.\n";
      exit  (1);
    }
    return loadJson(TOKENFILE);
}

/**
 * Updates the meta.json file with new connection data for the given entry name
 * (typically a domain alias). If any relevant fields (IP, port, protocol, domain)
 * differ from previously stored values, the metadata entry is updated, flagged with
 * `"changed": 1`, timestamped, and logged.
 *
 * The `location` field is preserved from existing data if it's set and non-empty,
 * otherwise it defaults to DEF_LOCATION.
 *
 * @param string $name     Logical identifier of the entry (e.g., domain alias)
 * @param string $domain   Target domain for use in proxy_pass
 * @param string $ip       New or updated public IP address
 * @param int    $port     Port number for proxy (typically 80 or 443)
 * @param string $protocol Protocol to use for proxy (either 'http' or 'https')
 * @return bool            True if the meta file was changed and saved; false otherwise
 */
function updateMeta(string $name, string $domain, string $ip, int $port, string $protocol): bool {
  $meta   = loadJson(METAFILE);           // Load current meta.json contents
  $resp   = "No changes for $name\n";     // Default response if no changes
  $result = false;                        // Flag to track if update occurred

  // New metadata values from the client
  $incoming = [
    'domain'   => $domain,
    'ip'       => $ip,
    'port'     => $port,
    'protocol' => $protocol,
  ];

  // Previously stored values for this entry
  $existing = $meta[$name] ?? [];

  // Check if any field has changed
  if (array_diff_assoc($incoming, $existing)) {
    $incoming['location'] = DEF_LOCATION;    // Default location path
    $incoming['time']     = date('c');       // Timestamp in ISO 8601 format

    // Preserve existing location if set and not empty
    if (array_key_exists('location', $existing) && !isEmpty($existing['location'])) {
      $incoming['location'] = $existing['location'];
    }

    $incoming['changed'] = 1;                // Mark for processing in updater
    logChange($name, $incoming);             // Log the update

    $meta[$name] = $incoming;                // Update in-memory data
    $result      = saveJson(METAFILE, $meta); // Write back to meta.json

    $resp = "Updated: [$protocol://$ip:$port] for $name\n";
  }

  http_response_code(200);                   // Always return success
  echo $resp;

  return $result;
}

/**
 * Checks whether a given IP address is currently blocked,
 * based on a JSON file containing expiration timestamps.
 *
 * @param string $ip       IP address to check
 * @return bool            True if the IP is currently blocked, false otherwise
 */
function isBlockedIP(string $ip): bool {
    // Load blocklist from file, or use an empty list if file is missing
    $blocklist = loadJson(BLOCKLIST_FILE);

    // Return true if IP exists in blocklist and its expiration time is in the future
    return isset($blocklist[$ip]) && time() < $blocklist[$ip];
}

/**
 * Ensures that the specified directory (or its parent) exists.
 *
 * If the provided path is a file, the function checks the directory it would belong to.
 * If the directory does not exist and $create is true, it attempts to create it.
 *
 * @param string $path   The path to check (can be a file or a directory)
 * @param bool   $create Whether to create the directory if it doesn't exist (default: true)
 * @return bool          True if the directory exists or was successfully created, false otherwise
 */
function ensureDirectoryExists(string $path, bool $create = true): bool {
    // Determine the directory: if it's not already a dir, get its parent
    $dir = is_dir($path) ? $path : dirname($path);

    // If the directory doesn't exist and we're allowed to create it
    if (!is_dir($dir) && $create) {
        return mkdir($dir, 0755, true);
    }

    // Directory exists or was not created intentionally
    return true;
}

/**
 * Appends a structured log entry to the log file.
 *
 * Each log entry is an associative array containing the provided data,
 * merged with a `name` identifier. The function preserves existing log
 * entries by appending to the decoded array before writing back to the file.
 *
 * @param string $name Identifier for the log entry (e.g. domain or config name)
 * @param array  $data Additional data to include in the log entry
 * @return void
 */
function logChange(string $name, array $data): void {
    // Load existing log data or start with an empty array
    $log = loadJson(LOGFILE);

    // Merge the name into the entry and append
    $log[] = array_merge(['name' => $name], $data);

    // Write updated log back to file
    saveJson(LOGFILE, $log);
}

/**
 * Records a failed authentication attempt for the given IP address.
 *
 * Tracks the number of failures in `failures.json`. If an IP reaches
 * the failure threshold (e.g., 3), it is added to the blocklist with
 * an expiration timestamp defined by `BLOCK_DURATION_SECONDS`.
 *
 * @param string $ip The IP address that failed authentication
 * @return void
 */
function recordFailure(string $ip): void {
  // Load the current failure count from the failures file
  $failures = loadJson(FAILURESFILE);

  // Increment failure count for the IP
  $failures[$ip] = ($failures[$ip] ?? 0) + 1;

  // If threshold exceeded, add IP to blocklist and reset failure count
  if ($failures[$ip] >= 3) {
      $blocklist      = loadJson(BLOCKLIST_FILE);
      $blocklist[$ip] = time() + BLOCK_DURATION_SECONDS;

      saveJson        (BLOCKLIST_FILE, $blocklist);
      unset           ($failures[$ip]); // Reset after blocking
  }

  // Save updated failure log
  saveJson  (FAILURESFILE, $failures);
}
