<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\ffmpeg_media\Trait\FileUsageCheckerTrait;
use Drupal\file\FileInterface;
use Drupal\file\FileUsage\FileUsageInterface;

/**
 * Manages file operations during video conversion.
 *
 * Handles file entity creation, path resolution between stream URIs
 * and local paths, temp file cleanup, and file usage tracking.
 *
 * @internal
 */
final class ConversionFileManager implements ConversionFileManagerInterface {

  use FileUsageCheckerTrait;

  /**
   * Constructs a ConversionFileManager.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   File system service.
   * @param \Drupal\file\FileUsage\FileUsageInterface $fileUsage
   *   File usage service.
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   Logger channel.
   */
  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly FileSystemInterface $fileSystem,
    protected readonly FileUsageInterface $fileUsage,
    protected readonly LoggerChannelInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function createFileEntity(
    string $path,
    string $mimeType,
  ): FileInterface {
    $fileStorage = $this->entityTypeManager->getStorage('file');
    $uri = $this->getStorageUri($path);

    $file = $fileStorage->create([
      'uri' => $uri,
      'filename' => basename($uri),
      'filemime' => $mimeType,
      'status' => 1,
    ]);
    $file->save();

    return $file;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function generateOutputPathInDirectory(
    string $sourcePath,
    string $outputDirectory,
    string $extension,
    int $mediaId,
    string $codecSuffix = '',
    string $fieldName = '',
  ): string {
    $baseName = pathinfo($sourcePath, PATHINFO_FILENAME);
    $suffix = $codecSuffix !== '' ? '_' . $codecSuffix : '';
    $filename = $baseName . '_converted' . $suffix . '.' . $extension;

    // Field-scoped: {dir}/{mediaId}/{fieldName}/{filename}.
    // Legacy (empty fieldName): {dir}/{mediaId}/{filename}.
    $fieldSegment = $fieldName !== '' ? '/' . $fieldName : '';

    if (str_contains($outputDirectory, '://')) {
      $this->validateStreamScheme($outputDirectory);
      [$scheme, $rest] = explode('://', $outputDirectory, 2);
      $rest = trim($rest, '/');
      $prefix = $scheme . '://';
      if ($rest !== '') {
        $prefix .= $rest . '/';
      }
      return $prefix . $mediaId . $fieldSegment . '/' . $filename;
    }

    $dir = rtrim($outputDirectory, DIRECTORY_SEPARATOR);
    return $dir . DIRECTORY_SEPARATOR . $mediaId . $fieldSegment . DIRECTORY_SEPARATOR . $filename;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getStorageUri(string $path): string {
    if (str_contains($path, '://')) {
      return $path;
    }

    $candidates = [
      'public://' => $this->fileSystem->realpath('public://'),
      'private://' => $this->fileSystem->realpath('private://'),
    ];

    foreach ($candidates as $scheme => $root) {
      if ($root === FALSE || $root === NULL || $root === '') {
        continue;
      }
      $root = rtrim($root, DIRECTORY_SEPARATOR);
      if ($path === $root) {
        return $scheme;
      }
      $prefix = $root . DIRECTORY_SEPARATOR;
      if (str_starts_with($path, $prefix)) {
        $relative = substr($path, strlen($prefix));
        $relative = str_replace(DIRECTORY_SEPARATOR, '/', $relative);
        return $scheme . $relative;
      }
    }

    $this->logger->warning(
      'Output path @path does not map to a stream wrapper; storing absolute path.',
      ['@path' => $path],
    );

    return $path;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getLocalPath(string $path): string {
    if (!str_contains($path, '://')) {
      return $path;
    }

    $directory = dirname($path);
    $realDir = $this->fileSystem->realpath($directory);
    if ($realDir === FALSE) {
      $this->fileSystem->prepareDirectory(
        $directory,
        FileSystemInterface::CREATE_DIRECTORY,
      );
      $realDir = $this->fileSystem->realpath($directory);
    }

    if ($realDir === FALSE) {
      $this->logger->error(
        'Unable to resolve local filesystem path for @path.',
        ['@path' => $path],
      );
      throw new \RuntimeException(sprintf(
        'Cannot resolve stream URI "%s" to a local filesystem path. Ensure the directory exists and is writable.',
        $path,
      ));
    }

    return $realDir . DIRECTORY_SEPARATOR . basename($path);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function cleanupTempFiles(array $tempFiles): void {
    foreach ($tempFiles as $tempFile) {
      if (file_exists($tempFile)) {
        if (!$this->fileSystem->delete($tempFile)) {
          $this->logger->warning(
            'Failed to delete temp file @path',
            ['@path' => $tempFile],
          );
        }
      }
    }
  }

  /**
   * Validates that a stream URI uses an allowed scheme.
   *
   * @param string $uri
   *   Stream URI to validate.
   *
   * @throws \InvalidArgumentException
   *   If the scheme is not allowed.
   */
  protected function validateStreamScheme(string $uri): void {
    [$scheme] = explode('://', $uri, 2);
    // Output storage allows only public and private wrappers. The temporary
    // wrapper is intentionally excluded because converted outputs must persist
    // beyond a single request. FfmpegCommandBuilder allows temporary:// for
    // intermediate pipeline files (e.g., two-pass log files) that are not
    // stored as permanent outputs.
    $allowed = ['public', 'private'];
    if (!in_array($scheme, $allowed, TRUE)) {
      throw new \InvalidArgumentException(sprintf(
        'Output directory uses disallowed stream wrapper "%s://". Allowed: %s.',
        $scheme,
        implode(', ', array_map(static fn(string $s): string => $s . '://', $allowed)),
      ));
    }
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  protected function getFileUsageService(): FileUsageInterface {
    return $this->fileUsage;
  }

}
