<?php

namespace LakeDrops\Docker4Drupal;

use LakeDrops\Component\Composer\BaseHandler;
use LakeDrops\Component\Dotenv\Dotenv;
use LakeDrops\DockerTraefik\Traefik;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Yaml\Yaml;

/**
 * Class Handler.
 *
 * @package LakeDrops\Docker4Drupal
 */
class Handler extends BaseHandler {

  /**
   * @var array
   */
  protected $options;

  /**
   * Configure Drupal Project for Docker.
   *
   * @param bool $overwrite
   *   Whether to overwrite existing config files.
   *
   * @throws \Twig_Error_Loader
   * @throws \Twig_Error_Runtime
   * @throws \Twig_Error_Syntax
   */
  public function configureProject($overwrite = FALSE) {

    // We only do the fancy stuff for developers.
    if (!$this->isDevMode()) {
      return;
    }

    $options = $this->getOptions();
    $fs = new Filesystem();
    $installationManager = $this->composer->getInstallationManager();

    if (isset($options['webroot'])) {
      if (!$fs->exists($options['webroot'])) {
        return;
      }
      $webRoot = $options['webroot'];
    }
    else {
      $drupalCorePackage = $this->getDrupalCorePackage();
      if (!$drupalCorePackage) {
        // We are called too early, Drupal core is not available yet.
        return;
      }
      $corePath = $installationManager->getInstallPath($drupalCorePackage);
      // Directory where Drupal's index.php is located.
      $webRoot = dirname($corePath);
    }

    // Directory where the root project is being created.
    $projectRoot = getcwd();
    // Directory where this plugin is being installed.
    $pluginRoot = $installationManager->getInstallPath($this->getPackage('lakedrops/docker4drupal'));

    // If the d8-project-scaffold plugin is present we only execute this one
    // if $force is TRUE. This way we can make sure that we get executed after
    // d8-project-scaffold.
    $settingsPath = $webRoot . '/sites/default';
    if ($this->getPackage('lakedrops/d8-project-scaffold')) {
      if (!$fs->exists($projectRoot . '/settings/default')) {
        return;
      }
      $settingsPath = 'settings/default';
    }

    // Provide all the required files.
    $twig_loader = new \Twig_Loader_Array([]);
    $twig = new \Twig_Environment($twig_loader);
    $options['webRoot'] = $webRoot . '/';
    $orig_ignored = FALSE;
    foreach ($this->getFiles($projectRoot, $webRoot, $settingsPath) as $template => $def) {
      if (!$fs->exists($def['dest'])) {
        $fs->mkdir($def['dest']);
      }
      $twig_loader->setTemplate($template, $template);
      $filename = $twig->render($template, $options);
      $file = $def['dest'] . '/' . $filename;
      if ($overwrite || !$fs->exists($file)) {
        $twig_loader->setTemplate($filename, file_get_contents($pluginRoot . '/templates/' . ($def['source'] ?? '') . $template . '.twig'));
        $rendered = $twig->render($filename, $options);
        if (!empty($def['add2yaml']) && isset($options[$filename])) {
          $yaml = Yaml::parse($rendered);
          /** @noinspection SlowArrayOperationsInLoopInspection */
          $yaml = array_merge_recursive($yaml, $options[$filename]);
          $rendered = Yaml::dump($yaml, 9, 2);
        }
        if ($fs->exists($file)) {
          if (md5_file($file) === md5($rendered)) {
            continue;
          }
          $orig_file = $file . '.orig';
          if ($fs->exists($orig_file)) {
            $fs->remove($orig_file);
          }
          $fs->rename($file, $orig_file);
          if (!$orig_ignored) {
            $this->git('ignore *.orig');
            $orig_ignored = TRUE;
          }
        }
        else if (empty($def['add2git'])) {
          $this->git('ignore ' . $filename);
        }
        file_put_contents($file, $rendered);
      }
      if (isset($def['link']) && ($def['link'] !== $settingsPath)) {
        $link = $def['link'] . '/' . $filename;
        if (!$fs->exists($link)) {
          $rel = substr($fs->makePathRelative($file, $projectRoot . '/' . $link), 3, -1);
          $fs->symlink($rel, $link);
        }
      }
      $fs->chmod($file, 0664);
    }

    // Make sure that settings.docker.php gets called from settings.php.
    $settingsPhpFile = $settingsPath . '/settings.php';
    if ($fs->exists($settingsPhpFile)) {
      $settingsPhp = file_get_contents($settingsPhpFile);
      if (strpos($settingsPhp, 'settings.docker.php') === FALSE) {
        $settingsPhp .= "\n\nif (file_exists(__DIR__ . '/settings.docker.php')) {\n  include __DIR__ . '/settings.docker.php';\n}\n";
        file_put_contents($settingsPhpFile, $settingsPhp);
      }
    }

    // Setup BackstopJS.
    $this->git('ignore tests/backstop/backstop_data/bitmaps_test');
    $this->git('ignore tests/backstop/backstop_data/html_report');
    $this->git('lfs track tests/backstop/**/*.png');

    $traefik = new Traefik($options['projectname']);
    $traefik->update();

    // Set permissions, see https://wodby.com/stacks/drupal/docs/local/permissions
    exec('setfacl -dR -m u:$(whoami):rwX -m u:82:rwX -m u:100:rX ' . $projectRoot . ' >/dev/null 2>&1');
    exec('setfacl -R -m u:$(whoami):rwX -m u:82:rwX -m u:100:rX ' . $projectRoot . ' >/dev/null 2>&1');
  }

  /**
   * List of files and settings on how to handle them.
   *
   * @param string $projectRoot
   *   Name of the project's root directory.
   * @param string $webRoot
   *   Name of the web's root directory.
   * @param string $settingsPath
   *   Name of the settings directory.
   *
   * @return array
   *   List of files.
   */
  protected function getFiles($projectRoot, $webRoot, $settingsPath): array {
    return [
      'settings.docker.php' => [
        'dest' => $projectRoot . '/' . $settingsPath,
        'link' => $webRoot . '/sites/default',
      ],
      'docker-compose.yml' => [
        'dest' => $projectRoot,
        'add2yaml' => TRUE,
      ],
      'aliases.drushrc.php' => [
        'dest' => $projectRoot . '/drush',
      ],
      'drushrc.php' => [
        'dest' => $projectRoot . '/drush',
      ],
      'default.site.yml' => [
        'dest' => $projectRoot . '/drush/sites',
        'add2yaml' => TRUE,
      ],
      'drush.yml' => [
        'dest' => $projectRoot . '/drush',
        'add2yaml' => TRUE,
      ],
      'wkhtmltox.sh' => [
        'dest' => $projectRoot . '/.docker-init',
      ],
      'backstop.json' => [
        'source' => 'tests/backstop/',
        'dest' => $projectRoot . '/tests/backstop',
        'add2yaml' => TRUE,
        'add2git' => TRUE,
      ]
    ];
  }

  /**
   * Retrieve options from composer.json "extra" configuration.
   *
   * @param null $key
   *   Optional name of an option to be received instead of the full set of options.
   *
   * @return array|string
   *   The options.
   */
  public function getOptions($key = NULL) {
    if ($this->options === NULL) {
      $env = new Dotenv('docker4drupal', $this->io);
      $projectname = getenv('COMPOSE_PROJECT_NAME');
      if (empty($projectname)) {$projectname = str_replace([' ', '-', '_', '.'], '', basename(getcwd()));
        $env->put('COMPOSE_PROJECT_NAME', $projectname);
      }
      $extra = $this->composer->getPackage()->getExtra() + ['docker4drupal' => []];
      $options = NestedArray::mergeDeep([
        'projectname' => $projectname,
        'ci_home' => '/home/gitlab-runner',
        'docker0' => [
          'ip' => ($this->isCiContext() || $this->isLocalDevMode()) ?
            $this->getDockerGateway() :
            $this->getLocalIpv4('docker0'),
        ],
        'live' => [
          'root' => '',
          'uri' => '',
          'host' => '',
          'user' => $env->receive('live_host_username', 'Remote username for host of the live site', getenv('USER')),
        ],
        'drush' => [
          'sql' => [
            'tables' => [
              'structure' => [
                'cache',
                'cache_*',
                'history',
                'search_*',
                'sessions',
                'watchdog',
              ],
              'skip' => [
                'migration_*',
              ],
            ],
          ],
        ],
        'drupal' => [
          'version' => '8',
        ],
        'php' => [
          'version' => '7.0',
          'xdebug' => $this->isLocalDevMode() ? 1 : 0,
        ],
        'webserver' => [
          'type' => 'apache',
        ],
        'varnish' => [
          'enable' => 0,
        ],
        'redis' => [
          'version' => '4.0',
        ],
        'dbbrowser' => [
          'type' => 'pma',
        ],
        'solr' => [
          'enable' => 0,
          'version' => '6.6',
        ],
        'node' => [
          'enable' => 0,
          'key' => '',
          'path' => '',
        ],
        'memcached' => [
          'enable' => 0,
        ],
        'rsyslog' => [
          'enable' => 0,
        ],
        'athenapdf' => [
          'enable' => 0,
          'key' => '',
        ],
        'blackfire' => [
          'enable' => 0,
          'id' => '',
          'token' => '',
        ],
        'webgrind' => [
          'enable' => 0,
        ],
        'wkhtmltox' => [
          'enable' => 0,
        ],
      ], $extra['docker4drupal']);

      if ($this->isCiContext() || $this->isLocalDevMode()) {
        $projectRoot = $this->getDockerMountSource(getenv('CI_PROJECT_DIR'));
      }
      else {
        $projectRoot = getcwd();
      }

      // Check if SSH auth sockets are supported.
      $ssh_auth_sock = getenv('SSH_AUTH_SOCK');
      $options['php']['ssh'] = !empty($ssh_auth_sock);
      if ($options['php']['ssh']) {
        $options['php']['ssh_auth_sock'] = ($this->isCiContext() || $this->isLocalDevMode()) ?
          $this->getDockerMountSource('/ssh-agent') :
          '$SSH_AUTH_SOCK';
      }

      $options['projectroot'] = $projectRoot;

      $this->options = $env->replaceEnvironmentVariables($options);
      $env->put('PHP_VERSION', $this->options['php']['version'], TRUE);
    }
    if ($key !== NULL) {
      return $this->options[$key];
    }
    return $this->options;
  }

  /**
   * Determine local ipv4 address.
   *
   * @param string|null $interface
   *   The name of the interface for which to determine the ipv4 address.
   *
   * @return string|array
   *   The ipv4 address(es).
   */
  private function getLocalIpv4($interface = NULL) {
    $out = explode(PHP_EOL, shell_exec('LC_ALL=C /sbin/ifconfig'));
    $local_addrs = array();
    $ifname = 'unknown';
    foreach ($out as $str) {
      $matches = array();
      if (preg_match('/^([a-z0-9]+)(:\d{1,2})?(\s)+Link/', $str, $matches)) {
        $ifname = $matches[1];
        if ($matches[2] !== '') {
          $ifname .= $matches[2];
        }
      }
      elseif (preg_match('/inet addr:((?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:[.](?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3})\s/', $str, $matches)) {
        $local_addrs[$ifname] = $matches[1];
      }
    }

    if (!isset($interface)) {
      return $local_addrs;
    }
    return $local_addrs[$interface] ?? '127.0.0.1';
  }

  /**
   * @return string
   */
  private function getDockerGateway(): string {
    $container = $this->readContainerConfig();
    return $container['NetworkSettings']['Gateway'];
  }

  /**
   * @param $projectRoot
   *
   * @return string
   */
  private function getDockerMountSource($projectRoot): string {
    $currentDir = getcwd();
    $container = $this->readContainerConfig();
    foreach ($container['Mounts'] as $mount) {
      if (empty($projectRoot)) {
        if ($currentDir === $mount['Destination']) {
          return $mount['Source'];
        }
      }
      else if (strpos($projectRoot, $mount['Destination']) === 0) {
        return $mount['Source'] . substr($projectRoot, strlen($mount['Destination']));
      }
    }
    return getcwd();
  }

  private function readContainerConfig() {
    try {
      $output = [];
      exec('basename "$(cat /proc/1/cpuset)"', $output);
      $id = reset($output);
      $output = [];
      exec('docker container inspect ' . $id, $output);
      return json_decode(implode('', $output), TRUE)[0];
    }
    catch (\Exception $ex) {
      // Ignore.
    }
    return [];
  }

}