using Microsoft.Extensions.Options;

namespace TeleWave.Infrastructure.Media;

/// <summary>
/// Единая точка резолва путей хранилища + защита от path traversal. Любой путь, собранный из
/// внешних данных (имя загруженного файла, имя из inbox/), проверяется на нахождение внутри корня.
/// </summary>
public sealed class MediaPathResolver
{
    private readonly string _root;

    public MediaPathResolver(IOptions<StorageOptions> options)
    {
        _root = Path.GetFullPath(options.Value.RootPath);
        InboxDir = Path.Combine(_root, "inbox");
        ManualDir = Path.Combine(_root, "manual");
        UploadsDir = Path.Combine(_root, "uploads");
        OriginalsDir = Path.Combine(_root, "originals");
        AssetsDir = Path.Combine(_root, "assets");
        BumpersDir = Path.Combine(_root, "bumpers");
        ImagesDir = Path.Combine(_root, "images");
    }

    public string InboxDir { get; }

    /// <summary>Ручной inbox: сканером не разбирается, файлы забирает админ из UI сразу в шоу.</summary>
    public string ManualDir { get; }
    public string UploadsDir { get; }
    public string OriginalsDir { get; }
    public string AssetsDir { get; }

    /// <summary>Сырые файлы шаблонов заставок (фон/музыка) по каналам — не режутся на HLS.</summary>
    public string BumpersDir { get; }

    /// <summary>Общий реестр изображений (галерея): файлы images/{imageId}{ext}.</summary>
    public string ImagesDir { get; }

    public void EnsureDirectories()
    {
        Directory.CreateDirectory(InboxDir);
        Directory.CreateDirectory(ManualDir);
        Directory.CreateDirectory(UploadsDir);
        Directory.CreateDirectory(OriginalsDir);
        Directory.CreateDirectory(AssetsDir);
        Directory.CreateDirectory(BumpersDir);
        Directory.CreateDirectory(ImagesDir);
    }

    public string BumperTemplateDir(Guid templateId) =>
        EnsureWithinRoot(Path.Combine(BumpersDir, templateId.ToString("N")));

    /// <summary>Путь к файлу блока заставки (kind — «audio»/«background», extension — с точкой).</summary>
    public string BumperTemplateFilePath(Guid templateId, string kind, string extension) =>
        EnsureWithinRoot(Path.Combine(BumpersDir, templateId.ToString("N"), kind + extension));

    /// <summary>Путь к файлу изображения общего реестра (extension — с точкой).</summary>
    public string ImagePath(Guid imageId, string extension) =>
        EnsureWithinRoot(Path.Combine(ImagesDir, imageId.ToString("N") + extension));

    public string OriginalPath(Guid assetId, string extension) =>
        EnsureWithinRoot(Path.Combine(OriginalsDir, assetId.ToString("N") + extension));

    public string AssetDir(Guid assetId) =>
        EnsureWithinRoot(Path.Combine(AssetsDir, assetId.ToString("N")));

    public static string AssetRelativePath(Guid assetId) => $"assets/{assetId:N}";

    /// <summary>Путь к файлу сегмента внутри каталога ассета (имя файла проверяется на traversal).</summary>
    public string SegmentPath(Guid assetId, string fileName)
    {
        var assetDir = AssetDir(assetId);
        return EnsureWithin(assetDir, Path.Combine(assetDir, fileName));
    }

    /// <summary>Резолвит имя файла внутри uploads/ (токен загрузки), проверяя выход за пределы каталога.</summary>
    public string UploadPath(string token) =>
        EnsureWithin(UploadsDir, Path.Combine(UploadsDir, token));

    /// <summary>Резолвит имя файла внутри inbox/, проверяя выход за пределы каталога.</summary>
    public string InboxPath(string fileName) =>
        EnsureWithin(InboxDir, Path.Combine(InboxDir, fileName));

    /// <summary>
    /// Резолвит путь внутри manual/. Путь относительный и может содержать подкаталоги — качалки
    /// раскладывают файлы по папкам, — поэтому проверка на выход за пределы каталога здесь
    /// обязательна: строка приходит из запроса.
    /// </summary>
    public string ManualPath(string relativePath) =>
        EnsureWithin(ManualDir, Path.Combine(ManualDir, relativePath));

    private string EnsureWithinRoot(string candidate) => EnsureWithin(_root, candidate);

    private static string EnsureWithin(string baseDir, string candidate)
    {
        var full = Path.GetFullPath(candidate);
        var normalizedBase = baseDir.EndsWith(Path.DirectorySeparatorChar)
            ? baseDir
            : baseDir + Path.DirectorySeparatorChar;

        if (
            !full.StartsWith(normalizedBase, StringComparison.Ordinal)
            && !string.Equals(full, baseDir, StringComparison.Ordinal)
        )
            throw new UnauthorizedAccessException(
                $"Путь '{candidate}' выходит за пределы каталога хранилища."
            );

        return full;
    }
}
