<?php
namespace nur\tools\compctl;

use nur\A;
use nur\b\ValueException;
use nur\file;
use nur\json;
use nur\msg;
use nur\path;
use nur\SL;
use nur\str;
use nur\yaml;

class ComposerFile {
  function __construct(string $composerFile, ?string $projdir=null, bool $ensureExists=true) {
    if ($ensureExists && !file_exists($composerFile)) {
      if ($projdir === null) $projdir = dirname($composerFile);
      $message = path::ppath($projdir).": aucune configuration n'a été trouvée";
      throw new ValueException($message);
    }
    $this->composerFile = $composerFile;
  }

  /** @var string */
  protected $composerFile;

  function getComposerFile(): string {
    return $this->composerFile;
  }

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

  protected function load(): array {
    if ($this->data === null) {
      $this->data = json::load($this->composerFile);
    }
    return $this->data;
  }

  function getv(string $pkey="", $default=null) {
    $this->load();
    return A::pget($this->data, $pkey, $default);
  }

  function geta(string $pkey="", ?array $default=[]): ?array {
    return A::withn($this->getv($pkey, $default));
  }

  function getRequires(): array {
    return $this->geta("require");
  }

  function getRepositories(): array {
    return $this->geta("repositories");
  }

  /** construire un fichier de configuration à partir du fichier composer */
  function initConfig(string $configFile): ?ConfigFile {
    if (file_exists($configFile)) return null;
    $requires = $this->getRequires();
    $php = A::get($requires, "php", ">=7.4");
    switch ($php) {
    case ">=8.2":
      $composer = [
        "php_min" => "8.2",
        "php_max" => "8.3",
        "image" => "pubdocker.univ-reunion.fr/image/phpbuilder:d12",
      ];
      break;
    case ">=7.4":
      $composer = [
        "php_min" => "7.4",
        "php_max" => "8.0",
        "image" => "pubdocker.univ-reunion.fr/image/phpbuilder:d11",
      ];
      break;
    case ">=7.3":
      $composer = [
        "php_min" => "7.3",
        "php_max" => "8.0",
        "image" => "pubdocker.univ-reunion.fr/image/phpbuilder:d10",
      ];
      break;
    case ">=7.0":
      $composer = [
        "php_min" => "7.0",
        "php_max" => "7.1",
        "image" => "pubdocker.univ-reunion.fr/image/phpbuilder:d9",
      ];
      break;
    default:
      msg::warning("$php: impossible de calculer la valeur de \$composer, choix du support de PHP 7.4");
      $composer = [
        "php_min" => "7.4",
        "php_max" => "8.0",
        "image" => "pubdocker.univ-reunion.fr/image/phpbuilder:d11",
      ];
      break;
    }
    $defaults = ConfigFile::DEFAULT_CONFIG["defaults"];
    $require = null;
    foreach ($requires as $libname => $version) {
      $isNur = str::starts_with("nur/", $libname);
      $isLib = str::starts_with("lib/", $libname);
      if (!$isNur && !$isLib) continue;
      if (str::starts_with("dev-", $version)
        || str::ends_with("-dev", $version)) {
        # version de développement, laisser se calculer les valeurs par défaut
        $require[$libname] = null;
      } else {
        $require[$libname] = $version;
      }
    }
    $branches = array_fill_keys(array_keys($defaults), null);
    $config = [
      "composer" => $composer,
      #"defaults" => $defaults,
      "require" => $require,
      "branch" => $branches,
    ];
    $contents = yaml::with($config);
    # pour la joliesse, enlever les ': null'
    $contents = preg_replace('/: null$/m', ":", $contents);

    $writer = file::with($configFile)->getWriter();
    $writer->pnl("# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8");
    $writer->putContents($contents);
    return new ConfigFile($configFile);
  }

  function computeComposer(ConfigFile $config, string $branch): array {
    $data = $orig = $this->load();

    [$deps, $isUrls, $notFoundUrls,
    ] = $config->getDeps($branch);
    if ($deps === null) {
      msg::warning("$branch: aucune définition trouvée pour cette branche");
      return [false, $orig];
    }
    foreach ($notFoundUrls as $url) {
      msg::warning("$url: project not found. not updating repositories");
    }

    # nettoyer les chemins pour chacune des dépendances de la config
    $crequires = A::get($data, "require", []);
    $crepositories = A::get($data, "repositories");
    if ($crepositories === null) {
      $crepositories = [[
        "type" => "composer",
        "url" => "https://repos.univ-reunion.fr/composer",
      ]];
    }
    foreach ($crepositories as $key => $crepository) {
      if (A::get($crepository, "type") !== "path") continue;
      $url = A::get($crepository, "url");
      if (A::get($isUrls, $url)) {
        unset($crepositories[$key]);
        continue;
      }
      # si un projet nur/ ou lib/ se trouve dans require, le prendre en compte
      # en d'autres termes, on nettoie les chemins qui n'ont plus de correspondance...
      $projname = basename($url);
      $libname = null;
      if (str::del_prefix($projname, "nur-")) {
        $libname = "nur/$projname";
      } elseif (str::del_prefix($projname, "lib-")) {
        $libname = "lib/$projname";
      }
      if ($libname !== null && array_key_exists($libname, $crequires)) {
        unset($crepositories[$key]);
      }
    }
    $data["repositories"] = $crepositories;
    # puis recalculer la liste des chemins
    $crepositories = [];
    foreach ($deps as $dep) {
      if ($dep["link"]) {
        $crepositories[] = [
          "type" => $dep["type"],
          "url" => $dep["url"],
        ];
      }
    }
    $data["repositories"] = SL::merge($crepositories, $data["repositories"]);
    # nettoyer les dépendances pour chacune des dépendances de la config
    foreach ($crequires as $libname => $version) {
      if (array_key_exists($libname, $deps)) {
        unset($crequires[$libname]);
      }
    }
    $data["require"] = $crequires;
    # puis installer les dépendances de la config
    $crequires = [];
    foreach ($deps as $dep) {
      $crequires[$dep["libname"]] = $dep["version"];
    }
    $data["require"] = SL::merge($crequires, $data["require"]);
    # retourner le nouveau contenu (qu'il faudra sérialiser)
    return [$data != $orig, $data];
  }

  protected function _save(array $data, bool $modified, bool $rewrite, bool $forceRewrite): bool {
    $contents = json::with($data, json::INDENT_TABS);
    if ($rewrite) {
      $contents .= "\n";
      if ($modified || $forceRewrite) file::with($this->composerFile)->putContents($contents);
      else msg::info("Aucune modifications effectuée");
    } else {
      if ($contents) echo "$contents\n";
    }
    return $modified;
  }

  function sync(ConfigFile $config, string $branch, bool $rewrite=false, bool $forceRewrite=false): bool {
    [$modified, $data] = $this->computeComposer($config, $branch);
    return $this->_save($data, $modified, $rewrite, $forceRewrite);
  }

  function setPkey(string $pkey, $value, bool $rewrite=false, bool $forceRewrite=false): bool {
    $data = $orig = $this->load();
    A::pset($data, $pkey, $value);
    return $this->_save($data, $data != $orig, $rewrite, $forceRewrite);
  }

  function delPkey(string $pkey, bool $rewrite=false, bool $forceRewrite=false): bool {
    $data = $orig = $this->load();
    A::pdel($data, $pkey);
    return $this->_save($data, $data != $orig, $rewrite, $forceRewrite);
  }

  function updateDep(string $dep, string $version, bool $rewrite=false, bool $forceRewrite=false): bool {
    return $this->setPkey("require.$dep", $version, $rewrite, $forceRewrite);
  }
}
