<?php declare(strict_types=1);
/**
 * @author Ryan Spaeth <rspaeth@spaethtech.com>
 * @copyright 2025 - Spaeth Technologies, Archous Networks
 */

namespace SpaethTech\UCRM\SDK;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use PDO;
use Psr\Container\ContainerInterface;
use SpaethTech\UCRM\SDK\Configs\ServerParams;
use SpaethTech\UCRM\SDK\Enums\ClientServiceWidgets;

final class Server
{
  private PDO $unmsDB;
  private PDO $ucrmDB;

  private ServerParams $parameters;

  public function __construct(private ?ContainerInterface $container = null)
  {
    if (!file_exists($file = "/usr/src/ucrm/app/config/parameters.yml"))
      die("The Server class can only be used within the UCRM system.");

    $this->parameters = new ServerParams($this);
  }

  #region CONTAINER

  public function getContainer(): ?ContainerInterface
  {
    return $this->container;
  }

  #endregion

  #region MODIFIED

  private const MODIFIED_FILE = "/usr/src/ucrm/modified.json";

  private function setModified(string $file): self
  {
    if (!file_exists(self::MODIFIED_FILE))
      file_put_contents(self::MODIFIED_FILE, "{}");

    $modified = json_decode(file_get_contents(self::MODIFIED_FILE), true);

    if (!array_key_exists("files", $modified))
      $modified["files"] = [];

    if (!in_array($file, $modified["files"]))
      $modified["files"][$file] = true;

    file_put_contents(self::MODIFIED_FILE, json_encode(
      $modified,
      JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
    ));

    return $this;
  }

  private function getModified(string $file): bool
  {
    if (!file_exists(self::MODIFIED_FILE))
      return false;

    $modified = json_decode(file_get_contents(self::MODIFIED_FILE), true);

    if (!array_key_exists("files", $modified))
      return false;

    if (!in_array($file, $modified["files"]))
      return false;

    return true;
  }

  public function anyModified(): bool
  {
    if (!file_exists(self::MODIFIED_FILE))
      return false;

    $modified = json_decode(file_get_contents(self::MODIFIED_FILE), true);

    if (!array_key_exists("files", $modified))
      return false;

    if (count($modified["files"]) === 0)
      return false;

    return true;
  }

  #endregion

  public function getParams(): ServerParams
  {
    return $this->parameters;
  }

  public function getInstanceId(): string
  {
    return $this->getSetting("instanceId");
  }

  public function getHostname(): string
  {
    return $this->getSetting("hostname");
  }


  #region DATABASE

  private function createPDO(): PDO
  {
    $host = $this->parameters->getDatabaseHost();
    $port = $this->parameters->getDatabasePort();
    $name = $this->parameters->getDatabaseName();
    $user = $this->parameters->getDatabaseUser();
    $pass = $this->parameters->getDatabasePassword();

    $dsn = "pgsql:host=$host;port=$port;dbname=$name";
    return new PDO($dsn, $user, $pass, [
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
      PDO::ATTR_EMULATE_PREPARES => false,
    ]);
  }

  public function getUnmsDB(): PDO
  {
    if (!isset($this->unmsDB)) {
      $this->unmsDB = $this->createPDO();
      $schema = $this->parameters->get("database_schema_unms");
      $this->unmsDB->exec("SET SEARCH_PATH TO $schema");
    }

    return $this->unmsDB;
  }

  public function getUcrmDB(): PDO
  {
    if (!isset($this->ucrmDB)) {
      $this->ucrmDB = $this->createPDO();
      $schema = $this->parameters->get("database_schema_ucrm");
      $this->ucrmDB->exec("SET SEARCH_PATH TO $schema");
    }

    return $this->ucrmDB;
  }

  public function getSetting(string $name, mixed $default = null): mixed
  {
    $pdo = $this->getUnmsDB();
    $stmt = $pdo->prepare("SELECT value FROM setting WHERE name = :name");
    $stmt->execute(["name" => $name]);
    $data = $stmt->fetch()["value"] ?? null;
    return !$data ? $default : json_decode($data);
  }

  #endregion

  #region CSP

  private const RE_CSP = "/^(\s*'content-security-policy' => ')(.*)(',)$/m";


  public function hasCSP(string $directive, string $value): bool
  {
    $src = "/usr/src/ucrm/src";
    $php = "$src/AppBundle/Subscriber/ResponseHeadersSubscriber.php";

    // Read the file contents.
    $contents = file_get_contents($php);
    $original = $contents;

    // Check for the CSP directive in the file.
    if (preg_match(self::RE_CSP, $contents, $matches)) {
      // Parse the existing CSP policies.
      $current = ContentSecurityPolicy::parse($matches[2]);
      $found = false;

      // Loop through the existing policies.
      foreach ($current as $csp) {
        // If the directive matches, add the new values.
        if ($csp->directive === $directive) {
          // echo "<pre>";
          // var_dump($csp->values);
          // echo "</pre>";
          foreach ($csp->values as $value)
            if ($value === $value)
              return true;
        }
      }

      // Return false if the CSP directive was not found or not modified.
      return $contents !== $original;
    }

    return false;
  }

  public function addCSP(string $directive, string ...$values): bool
  {
    // If no values are provided, do nothing.
    if (count($values) === 0)
      return false;

    $src = "/usr/src/ucrm/src";
    $php = "$src/AppBundle/Subscriber/ResponseHeadersSubscriber.php";
    $bak = "$php.bak";

    // Make a backup of the original file, if none already exists.
    if (!file_exists($bak))
      copy($php, $bak);

    // Read the file contents.
    $contents = file_get_contents($php);
    $original = $contents;

    // Check for the CSP directive in the file.
    if (preg_match(self::RE_CSP, $contents, $matches)) {
      // Parse the existing CSP policies.
      $current = ContentSecurityPolicy::parse($matches[2]);
      $found = false;

      // Loop through the existing policies.
      foreach ($current as $csp) {
        // If the directive matches, add the new values.
        if ($csp->directive === $directive) {
          // Add the new values.
          foreach ($values as $value)
            $csp->add($value);

          $found = true;
        }
      }

      // If the directive was not found, add a whole new policy.
      if (!$found)
        $current[] = new ContentSecurityPolicy($directive, $values);

      // Convert the policies to a string.
      $cstr = implode("; ", array_map(function ($csp) {
        return $csp->__toString();
      }, $current)) . ";";

      // Replace the existing CSP directive with the new policies.
      $contents = str_replace($matches[2], $cstr, $contents);

      // If the contents have changed, write the new contents to the file.
      // And mark the Server as modified.
      if ($contents !== $original) {
        file_put_contents($php, $contents);
        $this->setModified($php);

        // Return true if the CSP directive was found and modified.
        return true;
      }

      // Return false if the CSP directive was not found or not modified.
      return false;
    }

    // This is an error, the CSP directive should ALWAYS be present!
    return false;
  }

  public function delCSP(string $directive, string ...$values): bool
  {
    $src = "/usr/src/ucrm/src";
    $php = "$src/AppBundle/Subscriber/ResponseHeadersSubscriber.php";
    $bak = "$php.bak";

    // Make a backup of the original file, if none already exists.
    if (!file_exists($bak))
      copy($php, $bak);

    // Read the file contents.
    $contents = file_get_contents($php);
    $original = $contents;

    // Check for the CSP directive in the file.
    if (preg_match(self::RE_CSP, $contents, $matches)) {
      // Parse the existing CSP policies.
      $current = ContentSecurityPolicy::parse($matches[2]);
      $found = false;
      $deletes = [];

      // Loop through the existing policies.
      for ($i = 0; $i < count($current); $i++) {
        $csp = $current[$i];

        // If the directive matches, add the new values.
        if ($csp->directive === $directive) {
          // If no values are provided, delete the entire policy.
          if (count($values) === 0)
            $deletes[] = $i;
          else
            // Otherwise, delete the specified values.
            foreach ($values as $value)
              $csp->del($value);
        }
      }

      // If there are any policies to delete, delete them!
      if (count($deletes) > 0)
        $current = array_diff_key($current, array_flip($deletes));

      // Convert the policies to a string.
      $cstr = implode("; ", array_map(function ($csp) {
        return $csp->__toString();
      }, $current)) . ";";

      // Replace the existing CSP directive with the new policies.
      $contents = str_replace($matches[2], $cstr, $contents);

      // If the contents have changed, write the new contents to the file.
      // And mark the Server as modified.
      if ($contents !== $original) {
        file_put_contents($php, $contents);
        $this->setModified($php);

        // Return true if the CSP directive was found and modified.
        return true;
      }

      // Return false if the CSP directive was not found or not modified.
      return false;
    }

    // This is an error, the CSP directive should ALWAYS be present!
    return false;
  }

  #endregion

  #region TWIG

  public function clearTwigCache(): bool
  {
    exec("rm -rf /usr/src/ucrm/app/cache/prod/twig", $output, $code);
    return $code === 0;
  }

  #endregion

  #region PUBLIC URLS

  private string|null $publicUrl = null;

  public function setPublicUrl(string $url): self
  {
    $this->publicUrl = $url;
    return $this;
  }

  public function getPublicUrl(): string
  {
    if ($this->publicUrl === null)
      $this->publicUrl = "https://" . $this->getSetting("hostname");

    return $this->publicUrl;
  }

  public function getPublicUnmsUrl(): string
  {
    return $this->getPublicUrl() . "/nms";
  }

  public function getPublicUcrmUrl(): string
  {
    return $this->getPublicUrl() . "/crm";
  }

  public function getPublicUnmsApiUrl(): string
  {
    return $this->getPublicUnmsUrl() . "/api/v2.1";
  }

  public function getPublicUcrmApiUrl(): string
  {
    return $this->getPublicUcrmUrl() . "/api/v1.0";
  }

  #endregion

  public function getUnmsToken(): string
  {
    return $this->getParams()->get("unms_token");
  }

  public function getUcrmToken(): string
  {
    return $this->getUnmsToken();
  }

  // /usr/src/ucrm/app/Resources/views/client/services/show.html.twig

  // {% include 'client/services/components/view/plugin_widgets.html.twig' %}



  private function getViewsDir(): string
  {
    return "/usr/src/ucrm/app/Resources/views";
  }

  public function moveClientServiceWidget(
    ClientServiceWidgets $widget,
    ClientServiceWidgets $before
  ): self {
    $tpl = "{$this->getViewsDir()}/client/services/show.html.twig";
    $bak = "{$tpl}.bak";

    if (!file_exists($bak))
      copy($tpl, $bak);

    $contents = file_get_contents($tpl);
    $original = $contents;

    // Remove extra newlines from the inner body block.
    $re_block = '/({% block body %}\n)(.*)({% endblock %}\n\n)/ms';
    $contents = preg_replace_callback($re_block, function ($matches) {

      $inner = preg_replace("/^\n/m", "", $matches[2]);
      return $matches[1] . $inner . $matches[3];
    }, $contents);

    if (!str_contains($contents, $widget->value))
      return $this;

    if (!str_contains($contents, $before->value))
      return $this;

    $contents = str_replace("$widget->value\n", "", $contents);
    $contents = str_replace(
      "$before->value\n",
      "$widget->value\n$before->value\n",
      $contents
    );

    if ($contents !== $original) {
      file_put_contents($tpl, $contents);
      $this->setModified($tpl);
      $this->clearTwigCache();
    }

    return $this;
  }


  private Client $unmsClient;
  private Client $ucrmClient;


  public function getUnmsClient(): Client
  {
    if (!isset($this->unmsClient))
      $this->unmsClient = new Client([
        "base_uri" => "http://unms:8081/nms/api/v2.1/",
        "headers" => [
          "X-Auth-Token" => $this->getUnmsToken(),
        ],
      ]);

    return $this->unmsClient;
  }

  public function getUcrmClient(): Client
  {
    if (!isset($this->ucrmClient))
      $this->ucrmClient = new Client([
        "base_uri" => "http://localhost/crm/api/v1.0/",
        "headers" => [
          "X-Auth-App-Key" => $this->getUcrmToken(),
        ],
      ]);

    return $this->ucrmClient;
  }






  public function register(Plugin $plugin, string $provisioner_url): array|false
  {
    $logger = $plugin->getLogger();

    $data = [
      "instance_id" => $this->getSetting("instanceId"),
      "hostname" => $this->getSetting("hostname"),
      "unms_api_url" => $this->getPublicUnmsApiUrl(),
      "unms_api_key" => $this->getUnmsToken(),
    ];

    try {
      $client = new Client([
        "base_uri" => "$provisioner_url/api/v2/",
        "headers" => [
          "X-UISP-Instance-ID" => $this->getInstanceId(),
          "X-UISP-Hostname" => $this->getHostname(),
          "Content-Type" => "application/json"
        ]
      ]);
      $response = $client->post("auth", ["json" => $data]);
      $json = json_decode($response->getBody()->getContents(), true);
      $plugin->putJsonFile("data/auth.json", $json);
      return $json;
    } catch (GuzzleException $e) {
      $logger->error($e->getMessage());
      return false;
    }
  }



  public function uuid()
  {
    $data = random_bytes(16);

    $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
    $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10

    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
  }



}
