<?php

declare(strict_types=1);

namespace Drupal\native_observability_report\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Symfony\Component\Yaml\Yaml;

/**
 * Loads and resolves report narrative YAML definitions.
 */
final class ReportNarrativeLoader {

  /**
   * Creates a new report narrative loader instance.
   */
  public function __construct(
    private readonly ConfigFactoryInterface $configFactory,
    private readonly ModuleHandlerInterface $moduleHandler,
    private readonly FileSystemInterface $fileSystem,
  ) {
  }

  /**
   * Loads the active narrative definition for the requested language.
   *
   * @param string|null $languageCode
   *   The requested narrative language code.
   *
   * @return array
   *   The parsed narrative definition.
   */
  public function load(?string $languageCode = NULL): array {
    $config = $this->configFactory->get('native_observability_report.settings');
    $preferredLanguage = trim((string) ($languageCode ?: $config->get('fallback_language') ?: 'en'));
    $fallbackLanguage = trim((string) ($config->get('fallback_language') ?: 'en'));

    $candidatePaths = $this->buildCandidatePaths(
      $preferredLanguage,
      $fallbackLanguage,
      (bool) $config->get('use_custom_narrative_file'),
      trim((string) ($config->get('custom_narrative_file_path') ?? '')),
      (bool) $config->get('strict_fallback')
    );

    foreach ($candidatePaths as $candidatePath) {
      $realPath = $this->resolveRealPath($candidatePath);
      if ($realPath === NULL || !is_file($realPath)) {
        continue;
      }

      $parsed = Yaml::parseFile($realPath);
      if (is_array($parsed) && $parsed !== []) {
        return $parsed;
      }
    }

    return [];
  }

  /**
   * Returns metadata about the currently resolved narrative source.
   *
   * @param string|null $languageCode
   *   The requested language code.
   *
   * @return array
   *   Narrative source metadata.
   */
  public function inspect(?string $languageCode = NULL): array {
    $config = $this->configFactory->get('native_observability_report.settings');
    $preferredLanguage = trim((string) ($languageCode ?: $config->get('fallback_language') ?: 'en'));
    $fallbackLanguage = trim((string) ($config->get('fallback_language') ?: 'en'));

    $candidatePaths = $this->buildCandidatePaths(
      $preferredLanguage,
      $fallbackLanguage,
      (bool) $config->get('use_custom_narrative_file'),
      trim((string) ($config->get('custom_narrative_file_path') ?? '')),
      (bool) $config->get('strict_fallback')
    );

    foreach ($candidatePaths as $candidatePath) {
      $realPath = $this->resolveRealPath($candidatePath);
      if ($realPath !== NULL && is_file($realPath)) {
        $definition = Yaml::parseFile($realPath);
        return [
          'requested_language' => $preferredLanguage,
          'fallback_language' => $fallbackLanguage,
          'path' => $candidatePath,
          'resolved_path' => $realPath,
          'exists' => TRUE,
          'loaded' => is_array($definition) && $definition !== [],
        ];
      }
    }

    return [
      'requested_language' => $preferredLanguage,
      'fallback_language' => $fallbackLanguage,
      'path' => NULL,
      'resolved_path' => NULL,
      'exists' => FALSE,
      'loaded' => FALSE,
    ];
  }

  /**
   * Returns a value from the narrative definition using a dotted path.
   *
   * @param array $definition
   *   The narrative definition.
   * @param string $path
   *   The dotted lookup path.
   * @param mixed $default
   *   The default value when the path does not exist.
   *
   * @return mixed
   *   The resolved value.
   */
  public function getValue(array $definition, string $path, mixed $default = NULL): mixed {
    $segments = array_filter(explode('.', $path), static fn(string $segment): bool => $segment !== '');
    $cursor = $definition;

    foreach ($segments as $segment) {
      if (!is_array($cursor) || !array_key_exists($segment, $cursor)) {
        return $default;
      }
      $cursor = $cursor[$segment];
    }

    return $cursor;
  }

  /**
   * Builds candidate file paths in priority order.
   */
  private function buildCandidatePaths(
    string $preferredLanguage,
    string $fallbackLanguage,
    bool $useCustomNarrativeFile,
    string $customNarrativePath,
    bool $strictFallback,
  ): array {
    $paths = [];

    if ($useCustomNarrativeFile && $customNarrativePath !== '') {
      $paths = array_merge($paths, $this->expandLanguageVariants($customNarrativePath, $preferredLanguage, $fallbackLanguage, $strictFallback));
    }

    $modulePath = $this->moduleHandler->getModule('native_observability_report')->getPath();
    $defaultBasePath = $modulePath . '/resources/report_messages.en.yml';
    $paths = array_merge($paths, $this->expandLanguageVariants($defaultBasePath, $preferredLanguage, $fallbackLanguage, FALSE));

    return array_values(array_unique($paths));
  }

  /**
   * Expands a language-aware narrative path into prioritized candidates.
   */
  private function expandLanguageVariants(
    string $basePath,
    string $preferredLanguage,
    string $fallbackLanguage,
    bool $strictFallback,
  ): array {
    $paths = [];
    $normalized = trim($basePath);

    if (preg_match('/\.([a-z_]+)\.yml$/i', $normalized)) {
      $paths[] = preg_replace('/\.([a-z_]+)\.yml$/i', '.' . $preferredLanguage . '.yml', $normalized);
      if (!$strictFallback || $fallbackLanguage !== $preferredLanguage) {
        $paths[] = preg_replace('/\.([a-z_]+)\.yml$/i', '.' . $fallbackLanguage . '.yml', $normalized);
      }
      $paths[] = $normalized;
      return $paths;
    }

    $paths[] = $normalized;
    return $paths;
  }

  /**
   * Resolves an absolute filesystem path from a module or stream-wrapper path.
   */
  private function resolveRealPath(string $path): ?string {
    $path = trim($path);
    if ($path === '') {
      return NULL;
    }

    if (str_contains($path, '://')) {
      $resolved = $this->fileSystem->realpath($path);
      return $resolved !== FALSE ? $resolved : NULL;
    }

    if (str_starts_with($path, '/')) {
      return $path;
    }

    return DRUPAL_ROOT . '/' . ltrim($path, '/');
  }

}
