<?php

namespace Acquia\Blt\Robo\Common;

use Acquia\Blt\Robo\Config\ConfigInitializer;
use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector;
use loophp\phposinfo\Enum\FamilyName;
use loophp\phposinfo\OsInfo;
use Symfony\Component\Console\Input\ArgvInput;

/**
 * Attempts to detect various properties about the current hosting environment.
 *
 * @package Acquia\Blt\Robo\Common
 */
class EnvironmentDetector extends AcquiaDrupalEnvironmentDetector {

  /**
   * Get CI env name.
   *
   * In the case of multiple environment detectors declaring a CI env name, the
   * first one wins.
   *
   * @throws \ReflectionException
   */
  public static function getCiEnv() {
    $results = self::getSubclassResults(__FUNCTION__);
    if ($results) {
      return current($results);
    }

    if (getenv('PIPELINE_ENV')) {
      return 'pipelines';
    }

    if (getenv('GITLAB_CI_TOKEN')) {
      return 'codestudio';
    }

    return FALSE;
  }

  /**
   * Is CI.
   *
   * @throws \ReflectionException
   */
  public static function isCiEnv(): bool {
    return self::getCiEnv() || getenv('CI');
  }

  /**
   * Get the settings file include for the current CI environment.
   *
   * This may be provided by BLT, or via a Composer package that has provided
   * its own environment detector. In the case of multiple detectors providing a
   * settings file, the first one wins.
   *
   * @return string
   *   Settings file full path and filename.
   *
   * @throws \ReflectionException
   */
  public static function getCiSettingsFile(): string {
    $results = array_filter(self::getSubclassResults(__FUNCTION__));
    if ($results) {
      return current($results);
    }

    return sprintf("%s/vendor/acquia/blt/settings/%s.settings.php", dirname(DRUPAL_ROOT), self::getCiEnv());
  }

  /**
   * Is Pantheon.
   */
  public static function isPantheonEnv(): bool {
    return (bool) getenv('PANTHEON_ENVIRONMENT');
  }

  /**
   * Get Pantheon.
   */
  public static function getPantheonEnv() {
    return getenv('PANTHEON_ENVIRONMENT');
  }

  /**
   * Is Pantheon.
   */
  public static function isPantheonDevEnv(): bool {
    return self::getPantheonEnv() === 'dev';
  }

  /**
   * Is Pantheon.
   */
  public static function isPantheonStageEnv(): bool {
    return self::getPantheonEnv() === 'test';
  }

  /**
   * Is Pantheon.
   */
  public static function isPantheonProdEnv(): bool {
    return self::getPantheonEnv() === 'live';
  }

  /**
   * Is local.
   *
   * @throws \ReflectionException
   */
  public static function isLocalEnv(): bool {
    $results = self::getSubclassResults(__FUNCTION__);
    if ($results) {
      return TRUE;
    }

    return parent::isLocalEnv() && !self::isPantheonEnv() && !self::isCiEnv();
  }

  /**
   * Is dev.
   *
   * @throws \ReflectionException
   */
  public static function isDevEnv(): bool {
    $results = self::getSubclassResults(__FUNCTION__);
    if ($results) {
      return TRUE;
    }

    return self::isAhDevEnv() || self::isPantheonDevEnv();
  }

  /**
   * Is stage.
   *
   * @throws \ReflectionException
   */
  public static function isStageEnv(): bool {
    $results = self::getSubclassResults(__FUNCTION__);
    if ($results) {
      return TRUE;
    }

    return self::isAhStageEnv() || self::isPantheonStageEnv();
  }

  /**
   * Is prod.
   *
   * @throws \ReflectionException
   */
  public static function isProdEnv(): bool {
    $results = self::getSubclassResults(__FUNCTION__);
    if ($results) {
      return TRUE;
    }

    return self::isAhProdEnv() || self::isPantheonProdEnv();
  }

  /**
   * Is ACSF.
   */
  public static function isAcsfInited(): bool {
    return file_exists(DRUPAL_ROOT . "/sites/g");
  }

  /**
   * Get a standardized name of the current OS.
   *
   * @return string
   *   Name of the OS family.
   */
  public static function getPlatform(): string {
    return OsInfo::family();
  }

  /**
   * Get a unique ID for the current running environment.
   *
   * Should conform to UUIDs generated by sebhildebrandt/systeminformation.
   *
   * @see https://github.com/sebhildebrandt/systeminformation/blob/master/lib/osinfo.js
   *
   * @return string|null
   *   The machine UUID.
   */
  public static function getMachineUuid(): ?string {
    switch (self::getPlatform()) {
      case FamilyName::LINUX:
        return shell_exec('( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :');

      case FamilyName::DARWIN:
        $output = shell_exec('ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID');
        $parts = explode('=', str_replace('"', '', $output));
        return strtolower(trim($parts[1]));

      case FamilyName::WINDOWS:
        return shell_exec('%windir%\\System32\\reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid');

      default:
        return NULL;
    }
  }

  /**
   * OS name.
   *
   * @return string
   *   The OS name.
   */
  public static function getOsName(): string {
    return OsInfo::os();
  }

  /**
   * OS version.
   *
   * @return string
   *   The OS version.
   */
  public static function getOsVersion(): string {
    return OsInfo::version();
  }

  /**
   * OS is Darwin.
   */
  public static function isDarwin(): bool {
    return OsInfo::isApple();
  }

  /**
   * Get a standardized site / db name.
   *
   * On ACE or simple multisite installs, this is the site directory under
   * 'docroot/sites'.
   *
   * On ACSF, this is the ACSF db name.
   *
   * @param string $site_path
   *   Directory site path.
   *
   * @return string|null
   *   Site name.
   *
   * @throws \ReflectionException
   */
  public static function getSiteName(string $site_path): ?string {
    if (self::isAcsfEnv()) {
      return self::getAcsfDbName();
    }
    if (self::isAcsfInited() && self::isLocalEnv()) {
      global $argv;

      // When developing locally, we use the host name to determine which site
      // factory site is active. The hostname must have a corresponding entry
      // under the multisites key.
      $input = new ArgvInput($argv);
      $config_initializer = new ConfigInitializer(self::getRepoRoot(), $input);
      $blt_config = $config_initializer->initialize();

      // The hostname must match the pattern local.[site-name].com, where
      // [site-name] is a value in the multisites array.
      $domain_fragments = explode('.', getenv('HTTP_HOST'));
      if (isset($domain_fragments[1])) {
        $name = $domain_fragments[1];
        $acsf_sites = $blt_config->get('multisites');
        if (in_array($name, $acsf_sites, TRUE)) {
          return $name;
        }
      }

    }

    return str_replace('sites/', '', $site_path);
  }

  /**
   * Find the repo root.
   *
   * This isn't as trivial as it sounds, since a simple relative path
   * (__DIR__ . '/../../../../../../') won't work if this package is symlinked
   * using a Composer path repository, and this file can be invoked from both
   * web requests and BLT CLI calls.
   *
   * @return string
   *   The repo root as an absolute path.
   */
  public static function getRepoRoot(): string {
    if (defined('DRUPAL_ROOT')) {
      // This is a web or Drush request.
      return dirname(DRUPAL_ROOT);
    }

    // This is a BLT CLI call. Get the $repo_root that was set in
    // bin/blt-robo.php.
    // phpcs:ignore
    global $repo_root;
    return $repo_root;
  }

  /**
   * List detectable environments and whether they are currently active.
   *
   * @throws \ReflectionException
   */
  public static function getEnvironments(): array {
    return [
      'local' => self::isLocalEnv(),
      'dev' => self::isDevEnv(),
      'stage' => self::isStageEnv(),
      'prod' => self::isProdEnv(),
      'ci' => self::isCiEnv(),
      'ode' => self::isAhOdeEnv(),
      'ah_other' => self::isAhEnv() && !self::isDevEnv() && !self::isStageEnv() && !self::isAhOdeEnv() && !self::isProdEnv(),
    ];
  }

  /**
   * Call a given function in all EnvironmentDetector subclasses.
   *
   * Composer packages can provide their own version of an EnvironmentDetector
   * that inherits from this one. This allows for detection of new types of
   * environments not hardcoded in this class.
   *
   * @param string $functionName
   *   The function name to call in a subclass.
   *
   * @return array
   *   Results from each subclass function call (omits any null / false results)
   *
   * @throws \ReflectionException
   */
  private static function getSubclassResults(string $functionName): array {
    static $detectors;
    if (!isset($detectors)) {
      $autoload_file = self::getRepoRoot() . '/vendor/autoload.php';
      if (!file_exists($autoload_file)) {
        $detectors = [];
        return [];
      }
      // phpcs:ignore
      $autoloader = require $autoload_file;
      $classMap = $autoloader->getClassMap();
      $detectors = array_filter($classMap, function ($classPath) {
        return strpos($classPath, 'Blt/Plugin/EnvironmentDetector') !== FALSE;
      });
    }
    $results = [];
    foreach ($detectors as $detector => $classPath) {
      // Only call this method if it's been overridden by the child class.
      $detectorReflector = new \ReflectionMethod($detector, $functionName);
      if ($detectorReflector->getDeclaringClass()->getName() === $detector) {
        $results[] = call_user_func([$detector, $functionName]);
      }
    }
    return array_filter($results);
  }

}
